autoactivate loser gpu mode if necessary
[k8vacspelynky.git] / spelunky_main.vc
blob4df477c6a6ffe191254760356d44fdaa6e294c82
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';
27 #ifndef DISABLE_TIME_CHECK
28 # define DISABLE_TIME_CHECK
29 #endif
32 //#define MASK_TEST
34 //#define BIGGER_REPLAY_DATA
36 // ////////////////////////////////////////////////////////////////////////// //
37 #include "mapent/0all.vc"
38 #include "PlayerPawn.vc"
39 #include "PlayerPowerup.vc"
40 #include "GameLevel.vc"
43 // ////////////////////////////////////////////////////////////////////////// //
44 #include "uisimple.vc"
47 // ////////////////////////////////////////////////////////////////////////// //
48 class DebugSessionMovement : Object;
50 #ifdef BIGGER_REPLAY_DATA
51 array!(GameLevel::SavedKeyState) keypresses;
52 #else
53 array!ubyte keypresses; // on each frame
54 #endif
55 GameConfig playconfig;
57 transient int keypos;
58 transient int otherSeed, roomSeed;
61 override void Destroy () {
62   delete playconfig;
63   keypresses.length = 0;
64   ::Destroy();
68 final void resetReplay () {
69   keypos = 0;
73 #ifndef BIGGER_REPLAY_DATA
74 final void addKey (int kbidx, bool down) {
75   if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
76   keypresses[$] = kbidx|(down ? 0x80 : 0);
80 final void addEndOfFrame () {
81   keypresses[$] = 0xff;
85 enum {
86   NORMAL,
87   END_OF_FRAME,
88   END_OF_RECORD,
91 final int getKey (out int kbidx, out bool down) {
92   if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
93   if (keypos >= keypresses.length) return END_OF_RECORD;
94   ubyte b = keypresses[keypos++];
95   if (b == 0xff) return END_OF_FRAME;
96   kbidx = b&0x7f;
97   down = (b >= 0x80);
98   return NORMAL;
100 #endif
103 // ////////////////////////////////////////////////////////////////////////// //
104 class TempOptionsKeys : Object;
106 int[16*GameConfig::MaxActionBinds] keybinds;
107 int kbversion = 1;
110 // ////////////////////////////////////////////////////////////////////////// //
111 class Main : Object;
113 transient string dbgSessionStateFileName = "debug_game_session_state";
114 transient string dbgSessionMovementFileName = "debug_game_session_movement";
115 const float dbgSessionSaveIntervalInSeconds = 30;
117 GLTexture texTigerEye;
119 GameConfig config;
120 GameGlobal global;
121 SpriteStore sprStore;
122 BackTileStore bgtileStore;
123 GameLevel level;
125 int loserGPU;
127 int mouseX = int.min, mouseY = int.min;
128 int mouseLevelX = int.min, mouseLevelY = int.min;
129 bool renderMouseTile;
130 bool renderMouseRect;
132 enum StartMode {
133   Dead,
134   Alive,
135   Title,
136   Intro,
137   Stars,
138   Sun,
139   Moon,
142 StartMode startMode = StartMode.Intro;
143 bool pauseRequested;
144 bool helpRequested;
146 bool replayFastForward = false;
147 int replayFastForwardSpeed = 2;
148 bool saveGameSession = false;
149 bool replayGameSession = false;
150 enum Replay {
151   None,
152   Saving,
153   Replaying,
155 Replay doGameSavingPlaying = Replay.None;
156 float saveMovementLastTime = 0;
157 DebugSessionMovement debugMovement;
158 GameStats origStats; // for replaying
159 GameConfig origConfig; // for replaying
160 GameGlobal::SavedSeeds origSeeds;
162 int showHelp;
164 bool fullscreen;
165 transient bool allowRender = true;
168 #ifdef MASK_TEST
169 transient int maskSX, maskSY;
170 transient SpriteImage smask;
171 transient int maskFrame;
172 #endif
175 // ////////////////////////////////////////////////////////////////////////// //
176 final void saveKeyboardBindings () {
177   auto tok = SpawnObject(TempOptionsKeys);
178   foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
179   appSaveOptions(tok, "keybindings");
180   delete tok;
184 final void loadKeyboardBindings () {
185   auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
186   if (tok) {
187     if (tok.kbversion != TempOptionsKeys.default.kbversion) {
188       global.config.resetKeybindings();
189     } else {
190       foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
191     }
192     delete tok;
193   }
197 // ////////////////////////////////////////////////////////////////////////// //
198 void saveGameOptions () {
199   appSaveOptions(global.config, "config");
203 void loadGameOptions () {
204   auto cfg = appLoadOptions(GameConfig, "config");
205   if (cfg) {
206     auto oldHero = config.heroType;
207     auto tok = SpawnObject(TempOptionsKeys);
208     foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
209     delete global.config;
210     global.config = cfg;
211     config = cfg;
212     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
213     delete tok;
214     writeln("config loaded");
215     global.restartMusic();
216     global.fixVolumes();
217     //config.heroType = GameConfig::Hero.Spelunker;
218     config.heroType = oldHero;
219   }
220   // fix my bug
221   if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
225 // ////////////////////////////////////////////////////////////////////////// //
226 void saveGameStats () {
227   if (level.stats) appSaveOptions(level.stats, "stats");
231 void loadGameStats () {
232   auto stats = appLoadOptions(GameStats, "stats");
233   if (stats) {
234     delete level.stats;
235     level.stats = stats;
236   }
237   if (!level.stats) level.stats = SpawnObject(GameStats);
238   level.stats.global = global;
242 // ////////////////////////////////////////////////////////////////////////// //
243 struct UIPaneSaveInfo {
244   name id;
245   UIPane::SaveInfo nfo;
248 transient UIPane optionsPane; // either options, or binding editor
250 transient GameLevel::IVec2D optionsPaneOfs;
251 transient void delegate () saveOptionsDG;
253 transient array!UIPaneSaveInfo optionsPaneState;
256 final void saveCurrentPane () {
257   if (!optionsPane || !optionsPane.id) return;
259   // summon ghost
260   if (optionsPane.id == 'CheatFlags') {
261     if (instantGhost && level.ghostTimeLeft > 0) {
262       level.ghostTimeLeft = 1;
263     }
264   }
266   foreach (ref auto psv; optionsPaneState) {
267     if (psv.id == optionsPane.id) {
268       optionsPane.saveState(psv.nfo);
269       return;
270     }
271   }
272   // append new
273   optionsPaneState.length += 1;
274   optionsPaneState[$-1].id = optionsPane.id;
275   optionsPane.saveState(optionsPaneState[$-1].nfo);
279 final void restoreCurrentPane () {
280   if (optionsPane) optionsPane.setupHotkeys(); // why not?
281   if (!optionsPane || !optionsPane.id) return;
282   foreach (ref auto psv; optionsPaneState) {
283     if (psv.id == optionsPane.id) {
284       optionsPane.restoreState(psv.nfo);
285       return;
286     }
287   }
291 // ////////////////////////////////////////////////////////////////////////// //
292 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
293   if (!it.tagClass) return;
294   if (class!MapObject(it.tagClass)) {
295     level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
296     it.owner.closeMe = true;
297   }
301 // ////////////////////////////////////////////////////////////////////////// //
302 transient array!(class!MapObject) cheatItemsList;
305 final void fillCheatItemsList () {
306   cheatItemsList.length = 0;
307   cheatItemsList[$] = ItemProjectileArrow;
308   cheatItemsList[$] = ItemWeaponShotgun;
309   cheatItemsList[$] = ItemWeaponAshShotgun;
310   cheatItemsList[$] = ItemWeaponPistol;
311   cheatItemsList[$] = ItemWeaponMattock;
312   cheatItemsList[$] = ItemWeaponMachete;
313   cheatItemsList[$] = ItemWeaponWebCannon;
314   cheatItemsList[$] = ItemWeaponSceptre;
315   cheatItemsList[$] = ItemWeaponBow;
316   cheatItemsList[$] = ItemBones;
317   cheatItemsList[$] = ItemFakeBones;
318   cheatItemsList[$] = ItemFishBone;
319   cheatItemsList[$] = ItemRock;
320   cheatItemsList[$] = ItemJar;
321   cheatItemsList[$] = ItemSkull;
322   cheatItemsList[$] = ItemGoldenKey;
323   cheatItemsList[$] = ItemGoldIdol;
324   cheatItemsList[$] = ItemCrystalSkull;
325   cheatItemsList[$] = ItemShellSingle;
326   cheatItemsList[$] = ItemChest;
327   cheatItemsList[$] = ItemCrate;
328   cheatItemsList[$] = ItemLockedChest;
329   cheatItemsList[$] = ItemDice;
330   cheatItemsList[$] = ItemBasketBall;
334 final UIPane createCheatItemsPane () {
335   if (!level.player) return none;
337   UIPane pane = SpawnObject(UIPane);
338   pane.id = 'Items';
339   pane.sprStore = sprStore;
341   pane.width = 320*3-64;
342   pane.height = 240*3-64;
344   foreach (auto ipk; cheatItemsList) {
345     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
346     it.tagClass = ipk;
347   }
349   //optionsPaneOfs.x = 100;
350   //optionsPaneOfs.y = 50;
352   return pane;
356 // ////////////////////////////////////////////////////////////////////////// //
357 transient array!(class!MapObject) cheatEnemiesList;
360 final void fillCheatEnemiesList () {
361   cheatEnemiesList.length = 0;
362   cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
363   cheatEnemiesList[$] = EnemyBat;
364   cheatEnemiesList[$] = EnemySpiderHang;
365   cheatEnemiesList[$] = EnemySpider;
366   cheatEnemiesList[$] = EnemySnake;
367   cheatEnemiesList[$] = EnemyCaveman;
368   cheatEnemiesList[$] = EnemySkeleton;
369   cheatEnemiesList[$] = MonsterShopkeeper;
370   cheatEnemiesList[$] = EnemyZombie;
371   cheatEnemiesList[$] = EnemyVampire;
372   cheatEnemiesList[$] = EnemyFrog;
373   cheatEnemiesList[$] = EnemyGreenFrog;
374   cheatEnemiesList[$] = EnemyFireFrog;
375   cheatEnemiesList[$] = EnemyMantrap;
376   cheatEnemiesList[$] = EnemyScarab;
377   cheatEnemiesList[$] = EnemyFloater;
378   cheatEnemiesList[$] = EnemyBlob;
379   cheatEnemiesList[$] = EnemyMonkey;
380   cheatEnemiesList[$] = EnemyGoldMonkey;
381   cheatEnemiesList[$] = EnemyAlien;
382   cheatEnemiesList[$] = EnemyYeti;
383   cheatEnemiesList[$] = EnemyHawkman;
384   cheatEnemiesList[$] = EnemyUFO;
385   cheatEnemiesList[$] = EnemyYetiKing;
389 final UIPane createCheatEnemiesPane () {
390   if (!level.player) return none;
392   UIPane pane = SpawnObject(UIPane);
393   pane.id = 'Enemies';
394   pane.sprStore = sprStore;
396   pane.width = 320*3-64;
397   pane.height = 240*3-64;
399   foreach (auto ipk; cheatEnemiesList) {
400     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
401     it.tagClass = ipk;
402   }
404   //optionsPaneOfs.x = 100;
405   //optionsPaneOfs.y = 50;
407   return pane;
411 // ////////////////////////////////////////////////////////////////////////// //
412 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
415 final void fillCheatPickupList () {
416   cheatPickupList.length = 0;
417   cheatPickupList[$] = ItemPickupBombBag;
418   cheatPickupList[$] = ItemPickupBombBox;
419   cheatPickupList[$] = ItemPickupPaste;
420   cheatPickupList[$] = ItemPickupRopePile;
421   cheatPickupList[$] = ItemPickupShellBox;
422   cheatPickupList[$] = ItemPickupAnkh;
423   cheatPickupList[$] = ItemPickupCape;
424   cheatPickupList[$] = ItemPickupJetpack;
425   cheatPickupList[$] = ItemPickupUdjatEye;
426   cheatPickupList[$] = ItemPickupCrown;
427   cheatPickupList[$] = ItemPickupKapala;
428   cheatPickupList[$] = ItemPickupParachute;
429   cheatPickupList[$] = ItemPickupCompass;
430   cheatPickupList[$] = ItemPickupSpectacles;
431   cheatPickupList[$] = ItemPickupGloves;
432   cheatPickupList[$] = ItemPickupMitt;
433   cheatPickupList[$] = ItemPickupJordans;
434   cheatPickupList[$] = ItemPickupSpringShoes;
435   cheatPickupList[$] = ItemPickupSpikeShoes;
436   cheatPickupList[$] = ItemPickupTeleporter;
440 final UIPane createCheatPickupsPane () {
441   if (!level.player) return none;
443   UIPane pane = SpawnObject(UIPane);
444   pane.id = 'Pickups';
445   pane.sprStore = sprStore;
447   pane.width = 320*3-64;
448   pane.height = 240*3-64;
450   foreach (auto ipk; cheatPickupList) {
451     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
452     it.tagClass = ipk;
453   }
455   //optionsPaneOfs.x = 100;
456   //optionsPaneOfs.y = 50;
458   return pane;
462 // ////////////////////////////////////////////////////////////////////////// //
463 transient int instantGhost;
465 final UIPane createCheatFlagsPane () {
466   UIPane pane = SpawnObject(UIPane);
467   pane.id = 'CheatFlags';
468   pane.sprStore = sprStore;
470   pane.width = 320*3-64;
471   pane.height = 240*3-64;
473   instantGhost = 0;
475   UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
476   UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
477   UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
478   UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
479   UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
480   //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
481   UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
482   UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
483   UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
484   UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
485   UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
486   UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
487   //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
488   UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
489   UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
490   UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
491   UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
492   UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
494   optionsPaneOfs.x = 100;
495   optionsPaneOfs.y = 50;
497   return pane;
501 final UIPane createOptionsPane () {
502   UIPane pane = SpawnObject(UIPane);
503   pane.id = 'Options';
504   pane.sprStore = sprStore;
506   pane.width = 320*3-64;
507   pane.height = 240*3-64;
510   // this is buggy
511   //!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.");
514   UILabel.Create(pane, "VISUAL OPTIONS");
515     UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
516     UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
517     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).");
518     UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
519     auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
520     startfs.onValueChanged = delegate void (int newval) {
521       Video.showMouseCursor();
522       Video.closeScreen();
523       fullscreen = newval;
524       initializeVideo();
525     };
526     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).");
527     fsmode.names[$] = "REAL";
528     fsmode.names[$] = "SCALED";
529     fsmode.onValueChanged = delegate void (int newval) {
530       if (fullscreen) {
531         Video.showMouseCursor();
532         Video.closeScreen();
533         initializeVideo();
534       }
535     };
538   UILabel.Create(pane, "");
539   UILabel.Create(pane, "HUD OPTIONS");
540     UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
541     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.");
542     auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
543     halpha.step = 10;
545     auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
546     ialpha.step = 10;
549   UILabel.Create(pane, "");
550   UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
551     //!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.");
552     //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
553     UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
554     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.");
555     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.");
556     UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
557     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.");
558     UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
561   UILabel.Create(pane, "");
562   UILabel.Create(pane, "GAMEPLAY OPTIONS");
563     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.");
564     UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
565     UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
566     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!");
567     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.");
568     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.");
569     UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
570     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.");
571     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.");
572     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.");
573     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.");
574     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?");
575     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.");
576     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.");
577     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.");
578     UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
579     UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
580     UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
581     auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
582     rstl.names[$] = "RANDOM";
583     rstl.names[$] = "NORMAL";
584     rstl.names[$] = "BIZARRE";
587   UILabel.Create(pane, "");
588   UILabel.Create(pane, "WHIP OPTIONS");
589     UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
590     auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
591     whiptype.names[$] = "NORMAL";
592     whiptype.names[$] = "LONG";
593     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.");
596   UILabel.Create(pane, "");
597   UILabel.Create(pane, "PLAYER OPTIONS");
598     auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
599     herotype.names[$] = "SPELUNKY GUY";
600     herotype.names[$] = "DAMSEL";
601     herotype.names[$] = "TUNNEL MAN";
604   UILabel.Create(pane, "");
605   UILabel.Create(pane, "CHEAT OPTIONS");
606     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.");
607     auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
608     plrlit.names[$] = "NEVER";
609     plrlit.names[$] = "FORCED DARKNESS";
610     plrlit.names[$] = "ALWAYS";
611     UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
612     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'.");
613     rdark.names[$] = "NEVER";
614     rdark.names[$] = "DEFAULT";
615     rdark.names[$] = "ALWAYS";
616     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.");
617     rghost.step = 30;
618     rghost.getNameCB = delegate string (int val) {
619       if (val < 0) return "INSTANT";
620       if (val == 0) return "NEVER";
621       if (val < 120) return va("%d SEC", val);
622       if (val%60 == 0) return va("%d MIN", val/60);
623       if (val%60 == 30) return va("%d.5 MIN", val/60);
624       return va("%d MIN, %d SEC", val/60, val%60);
625     };
626     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.");
628   UILabel.Create(pane, "");
629   UILabel.Create(pane, "CHEAT START OPTIONS");
630     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.");
631     UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
632     UIIntEnum.Create(pane, &config.scumStartLife,  1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
633     UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
634     UIIntEnum.Create(pane, &config.scumStartRope,  1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
637   UILabel.Create(pane, "");
638   UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
639     auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC  : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
640     mm.names[$] = "SILENCE";
641     mm.names[$] = "RESTART";
642     mm.names[$] = "DON'T TOUCH";
644     mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
645     //mm.names[$] = "SILENCE";
646     mm.names[$] = "RESTART";
647     mm.names[$] = "DON'T TOUCH";
650   //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
651   /*
652   swstereo.onValueChanged = delegate void (int newval) {
653     SoundSystem.SwapStereo = newval;
654   };
655   */
657   UILabel.Create(pane, "");
658   UILabel.Create(pane, "SOUND CONTROL CENTER");
659     auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
660     rmusonoff.onValueChanged = delegate void (int newval) {
661       global.restartMusic();
662     };
664     UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
666     auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
667     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
669     rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
670     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
673   saveOptionsDG = delegate void () {
674     writeln("saving options");
675     saveGameOptions();
676   };
677   optionsPaneOfs.x = 42;
678   optionsPaneOfs.y = 0;
680   return pane;
684 final void createBindingsControl (UIPane pane, int keyidx) {
685   string kname, khelp;
686   switch (keyidx) {
687     case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
688     case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
689     case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
690     case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
691     case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
692     case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
693     case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
694     case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
695     case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
696     case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
697     case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
698     default: return;
699   }
700   int arridx = GameConfig.getKeyIndex(keyidx);
701   UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
705 final UIPane createBindingsPane () {
706   UIPane pane = SpawnObject(UIPane);
707   pane.id = 'KeyBindings';
708   pane.sprStore = sprStore;
710   pane.width = 320*3-64;
711   pane.height = 240*3-64;
713   createBindingsControl(pane, GameConfig::Key.Left);
714   createBindingsControl(pane, GameConfig::Key.Right);
715   createBindingsControl(pane, GameConfig::Key.Up);
716   createBindingsControl(pane, GameConfig::Key.Down);
717   createBindingsControl(pane, GameConfig::Key.Jump);
718   createBindingsControl(pane, GameConfig::Key.Run);
719   createBindingsControl(pane, GameConfig::Key.Attack);
720   createBindingsControl(pane, GameConfig::Key.Switch);
721   createBindingsControl(pane, GameConfig::Key.Pay);
722   createBindingsControl(pane, GameConfig::Key.Bomb);
723   createBindingsControl(pane, GameConfig::Key.Rope);
725   saveOptionsDG = delegate void () {
726     writeln("saving keys");
727     saveKeyboardBindings();
728   };
729   optionsPaneOfs.x = 120;
730   optionsPaneOfs.y = 140;
732   return pane;
736 // ////////////////////////////////////////////////////////////////////////// //
737 void clearGameMovement () {
738   debugMovement = SpawnObject(DebugSessionMovement);
739   debugMovement.playconfig = SpawnObject(GameConfig);
740   debugMovement.playconfig.copyGameplayConfigFrom(config);
741   debugMovement.resetReplay();
745 void saveGameMovement (string fname, optional bool packit) {
746   if (debugMovement) appSaveOptions(debugMovement, fname, packit);
747   saveMovementLastTime = GetTickCount();
751 void loadGameMovement (string fname) {
752   delete debugMovement;
753   debugMovement = appLoadOptions(DebugSessionMovement, fname);
754   debugMovement.resetReplay();
755   if (debugMovement) {
756     delete origStats;
757     origStats = level.stats;
758     origStats.global = none;
759     level.stats = SpawnObject(GameStats);
760     level.stats.global = global;
761     delete origConfig;
762     origConfig = config;
763     config = debugMovement.playconfig;
764     global.config = config;
765     global.saveSeeds(origSeeds);
766   }
770 void stopReplaying () {
771   if (debugMovement) {
772     global.restoreSeeds(origSeeds);
773   }
774   delete debugMovement;
775   saveGameSession = false;
776   replayGameSession = false;
777   doGameSavingPlaying = Replay.None;
778   if (origStats) {
779     delete level.stats;
780     origStats.global = global;
781     level.stats = origStats;
782     origStats = none;
783   }
784   if (origConfig) {
785     delete config;
786     config = origConfig;
787     global.config = origConfig;
788     origConfig = none;
789   }
793 // ////////////////////////////////////////////////////////////////////////// //
794 final bool saveGame (string gmname) {
795   return appSaveOptions(level, gmname);
799 final bool loadGame (string gmname) {
800   auto olddel = ImmediateDelete;
801   ImmediateDelete = false;
802   bool res = false;
803   auto stats = level.stats;
804   level.stats = none;
806   auto lvl = appLoadOptions(GameLevel, gmname);
807   if (lvl) {
808     //lvl.global.config = config;
809     delete level;
810     delete global;
812     level = lvl;
813     level.loserGPU = loserGPU;
814     global = level.global;
815     global.config = config;
817     level.sprStore = sprStore;
818     level.bgtileStore = bgtileStore;
821     level.onBeforeFrame = &beforeNewFrame;
822     level.onAfterFrame = &afterNewFrame;
823     level.onInterFrame = &interFrame;
824     level.onLevelExitedCB = &levelExited;
825     level.onCameraTeleported = &cameraTeleportedCB;
827     //level.viewWidth = Video.screenWidth;
828     //level.viewHeight = Video.screenHeight;
829     level.viewWidth = 320*3;
830     level.viewHeight = 240*3;
832     level.onLoaded();
833     level.centerViewAtPlayer();
834     teleportCameraAt(level.viewStart);
836     recalcCameraCoords(0);
838     res = true;
839   }
840   level.stats = stats;
841   level.stats.global = level.global;
843   ImmediateDelete = olddel;
844   CollectGarbage(true); // destroy delayed objects too
845   return res;
849 // ////////////////////////////////////////////////////////////////////////// //
850 float lastThinkerTime;
851 int replaySkipFrame = 0;
854 final void onTimePasses () {
855   float curTime = GetTickCount();
856   if (lastThinkerTime > 0) {
857     if (curTime < lastThinkerTime) {
858       writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
859       lastThinkerTime = curTime;
860       return;
861     }
862     if (replayFastForward && replaySkipFrame) {
863       level.accumTime = 0;
864       lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
865       replaySkipFrame = 0;
866     }
867     level.processThinkers(curTime-lastThinkerTime);
868   }
869   lastThinkerTime = curTime;
873 final void resetFramesAndForceOne () {
874   float curTime = GetTickCount();
875   lastThinkerTime = curTime;
876   level.accumTime = 0;
877   auto wasPaused = level.gamePaused;
878   level.gamePaused = false;
879   if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
880   level.processThinkers(GameLevel::FrameTime);
881   level.gamePaused = wasPaused;
882   //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
886 // ////////////////////////////////////////////////////////////////////////// //
887 private float currFrameDelta; // so level renderer can properly interpolate the player
888 private GameLevel::IVec2D camPrev, camCurr;
889 private GameLevel::IVec2D camShake;
890 private GameLevel::IVec2D viewCameraPos;
893 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
894   camPrev.x = pos.x;
895   camPrev.y = pos.y;
896   camCurr.x = pos.x;
897   camCurr.y = pos.y;
898   viewCameraPos.x = pos.x;
899   viewCameraPos.y = pos.y;
900   camShake.x = 0;
901   camShake.y = 0;
905 // call `recalcCameraCoords()` to get real camera coords after this
906 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
907   // check if camera is moved too far, and teleport it
908   if (doTeleport ||
909       (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
910        abs(camCurr.y-pos.y)/global.scale >= 16*4))
911   {
912     teleportCameraAt(pos);
913   } else {
914     camPrev.x = camCurr.x;
915     camPrev.y = camCurr.y;
916     camCurr.x = pos.x;
917     camCurr.y = pos.y;
918   }
919   camShake.x = level.shakeDir.x*global.scale;
920   camShake.y = level.shakeDir.y*global.scale;
924 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
925   currFrameDelta = frameDelta;
926   viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
927   viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
929   viewCameraPos.x += camShake.x;
930   viewCameraPos.y += camShake.y;
934 GameLevel::SavedKeyState savedKeyState;
936 final void pauseGame () {
937   if (!level.gamePaused) {
938     if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
939     level.gamePaused = true;
940     global.pauseAllSounds();
941   }
945 final void unpauseGame () {
946   if (level.gamePaused) {
947     if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
948     level.gamePaused = false;
949     level.gameShowHelp = false;
950     level.gameHelpScreen = 0;
951     //lastThinkerTime = 0;
952     global.resumeAllSounds();
953   }
954   pauseRequested = false;
955   helpRequested = false;
956   showHelp = false;
960 final void beforeNewFrame (bool frameSkip) {
961   /*
962   if (freeRide) {
963     level.disablePlayerThink = true;
965     int delta = 2;
966     if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
967     if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
968     if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
970     if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
971     if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
972     if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
973     if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
974   } else {
975     level.disablePlayerThink = false;
976     level.fixCamera();
977   }
978   */
979   level.fixCamera();
981   if (!level.gamePaused) {
982     // save seeds for afterframe processing
983     /*
984     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
985       debugMovement.otherSeed = global.globalOtherSeed;
986       debugMovement.roomSeed = global.globalRoomSeed;
987     }
988     */
990     if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
992 #ifdef BIGGER_REPLAY_DATA
993     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
994       debugMovement.keypresses.length += 1;
995       level.keysSaveState(debugMovement.keypresses[$-1]);
996       debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
997       debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
998     }
999 #endif
1001     if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
1002 #ifdef BIGGER_REPLAY_DATA
1003       if (debugMovement.keypos < debugMovement.keypresses.length) {
1004         level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1005         global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1006         global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1007         ++debugMovement.keypos;
1008       }
1009 #else
1010       for (;;) {
1011         int kbidx;
1012         bool down;
1013         auto code = debugMovement.getKey(out kbidx, out down);
1014         if (code == DebugSessionMovement::END_OF_RECORD) {
1015           // do this in main loop, so we can view totals
1016           //stopReplaying();
1017           break;
1018         }
1019         if (code == DebugSessionMovement::END_OF_FRAME) {
1020           break;
1021         }
1022         if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1023         level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1024       }
1025 #endif
1026     }
1027   }
1031 final void afterNewFrame (bool frameSkip) {
1032   if (!replayFastForward) replaySkipFrame = 0;
1034   if (level.gamePaused) return;
1036   if (!level.gamePaused) {
1037     if (doGameSavingPlaying != Replay.None) {
1038       if (doGameSavingPlaying == Replay.Saving) {
1039         replayFastForward = false; // just in case
1040 #ifndef BIGGER_REPLAY_DATA
1041         debugMovement.addEndOfFrame();
1042 #endif
1043         auto stt = GetTickCount();
1044         if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1045       } else if (doGameSavingPlaying == Replay.Replaying) {
1046         if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1047           replaySkipFrame = 1;
1048         }
1049       }
1050     }
1051   }
1053   //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1054   //SoundSystem.UpdateSounds();
1056   //if (!freeRide) level.fixCamera();
1057   setNewCameraPos(level.viewStart);
1058   /*
1059   prevCameraX = currCameraX;
1060   prevCameraY = currCameraY;
1061   currCameraX = level.cameraX;
1062   currCameraY = level.cameraY;
1063   // disable camera interpolation if the screen is shaking
1064   if (level.shakeX|level.shakeY) {
1065     prevCameraX = currCameraX;
1066     prevCameraY = currCameraY;
1067     return;
1068   }
1069   // disable camera interpolation if it moves too far away
1070   if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1071   if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1072   */
1073   recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1075   if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1076     pauseRequested = false;
1077     pauseGame();
1078     if (helpRequested) {
1079       helpRequested = false;
1080       level.gameShowHelp = true;
1081       level.gameHelpScreen = 0;
1082       showHelp = 2;
1083     } else {
1084       if (!showHelp) showHelp = true;
1085     }
1086     writeln("active objects in level: ", level.activeItemsCount);
1087     return;
1088   }
1092 final void interFrame (float frameDelta) {
1093   if (!config.interpolateMovement) return;
1094   recalcCameraCoords(frameDelta);
1098 final void cameraTeleportedCB () {
1099   teleportCameraAt(level.viewStart);
1100   recalcCameraCoords(0);
1104 // ////////////////////////////////////////////////////////////////////////// //
1105 #ifdef MASK_TEST
1106 final void setColorByIdx (bool isset, int col) {
1107   if (col == -666) {
1108     // missed collision: red
1109     Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1110   } else if (col == -999) {
1111     // superfluous collision: blue
1112     Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1113   } else if (col <= 0) {
1114     // no collision: yellow
1115     Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1116   } else if (col > 0) {
1117     // collision: green
1118     Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1119   }
1123 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1124   if (!frm) return;
1125   CollisionMask cm = CollisionMask.Create(frm, false);
1126   if (!cm) return;
1127   int scale = global.config.scale;
1128   int bx0, by0, bx1, by1;
1129   frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1130   Video.color = 0x7f_00_00_ff;
1131   Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1132   if (!cm.isEmptyMask) {
1133     //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1134     foreach (int iy; 0..cm.height) {
1135       foreach (int ix; 0..cm.width) {
1136         int v = cm.mask[ix, iy];
1137         foreach (int dx; 0..32) {
1138           int xx = ix*32+dx;
1139           if (v < 0) {
1140             Video.color = 0x3f_00_ff_00;
1141             Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1142           }
1143           v <<= 1;
1144         }
1145       }
1146     }
1147   } else {
1148     // bounding box
1149     /+
1150     foreach (int iy; 0..frm.tex.height) {
1151       foreach (int ix; 0..(frm.tex.width+31)/31) {
1152         foreach (int dx; 0..32) {
1153           int xx = ix*32+dx;
1154           //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1155           if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1156             setColorByIdx(true, col);
1157             if (col <= 0) Video.color = 0xaf_ff_ff_00;
1158           } else {
1159             Video.color = 0xaf_00_ff_00;
1160           }
1161           Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1162         }
1163       }
1164     }
1165     +/
1166     /*
1167     if (frm.bw > 0 && frm.bh > 0) {
1168       setColorByIdx(true, col);
1169       Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1170       Video.color = 0xff_00_00;
1171       Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1172     }
1173     */
1174   }
1175   delete cm;
1177 #endif
1180 // ////////////////////////////////////////////////////////////////////////// //
1181 transient int drawStats;
1182 transient array!int statsTopItem;
1185 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1186   auto sa = string(a.objName).toUpperCase;
1187   auto sb = string(b.objName).toUpperCase;
1188   return (sa < sb);
1192 final int getStatsTopItem () {
1193   return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1197 final void setStatsTopItem (int val) {
1198   if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1199   statsTopItem[drawStats] = val;
1203 final void resetStatsTopItem () {
1204   setStatsTopItem(0);
1208 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1209   sprStore.loadFont('sFontSmall');
1210   currX = 64;
1211   currY = 34;
1215 final int calcStatsVisItems () {
1216   int scale = 3;
1217   int currX, currY;
1218   statsDrawGetStartPosLoadFont(currX, currY);
1219   int endY = level.viewHeight-(currY*2);
1220   return max(1, endY/sprStore.getFontHeight(scale));
1224 int getStatsItemCount () {
1225   switch (drawStats) {
1226     case 2: return level.stats.totalKills.length;
1227     case 3: return level.stats.totalDeaths.length;
1228     case 4: return level.stats.totalCollected.length;
1229   }
1230   return -1;
1234 final void statsMoveUp () {
1235   int count = getStatsItemCount();
1236   if (count < 0) return;
1237   int visItems = calcStatsVisItems();
1238   if (count <= visItems) { resetStatsTopItem(); return; }
1239   int top = getStatsTopItem();
1240   if (!top) return;
1241   setStatsTopItem(top-1);
1245 final void statsMoveDown () {
1246   int count = getStatsItemCount();
1247   if (count < 0) return;
1248   int visItems = calcStatsVisItems();
1249   if (count <= visItems) { resetStatsTopItem(); return; }
1250   int top = getStatsTopItem();
1251   //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1252   top = clamp(top+1, 0, count-visItems);
1253   setStatsTopItem(top);
1257 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1258   arr.sort(&totalsNameCmpCB);
1259   int scale = 3;
1261   int currX, currY;
1262   statsDrawGetStartPosLoadFont(currX, currY);
1264   int endY = level.viewHeight-(currY*2);
1265   int visItems = calcStatsVisItems();
1267   if (arr.length <= visItems) resetStatsTopItem();
1269   int topItem = getStatsTopItem();
1271   // "upscroll" mark
1272   if (topItem > 0) {
1273     Video.color = 0x3f_ff_ff_00;
1274     auto spr = sprStore['sPageUp'];
1275     spr.frames[0].blitAt(currX-28, currY, scale);
1276   }
1278   // "downscroll" mark
1279   if (topItem+visItems < arr.length) {
1280     Video.color = 0x3f_ff_ff_00;
1281     auto spr = sprStore['sPageDown'];
1282     spr.frames[0].blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1283   }
1285   Video.color = 0xff_ff_00;
1286   int hiColor = 0x00_ff_00;
1287   int hiColor1 = 0xf_ff_ff;
1289   int it = topItem;
1290   while (it < arr.length && visItems-- > 0) {
1291     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);
1292     currY += sprStore.getFontHeight(scale);
1293     ++it;
1294   }
1298 void drawStatsScreen () {
1299   int deathCount, killCount, collectCount;
1301   sprStore.loadFont('sFontSmall');
1303   Video.color = 0xff_ff_ff;
1304   level.drawTextAtS3Centered(240-2-8, "ESC-RETURN  F10-QUIT  CTRL+DEL-SUICIDE");
1305   level.drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
1307   Video.color = 0xff_ff_00;
1308   int hiColor = 0x00_ff_00;
1310   switch (drawStats) {
1311     case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1312     case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1313     case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1314   }
1316   if (drawStats > 1) {
1317     // turn off
1318     foreach (ref auto i; statsTopItem) i = 0;
1319     drawStats = 0;
1320     return;
1321   }
1323   foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1324   foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1325   foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1327   int currX = 64;
1328   int currY = 96;
1329   int scale = 3;
1331   sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1332   currY += sprStore.getFontHeight(scale);
1334   int gw = level.stats.gamesWon;
1335   sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1336   currY += sprStore.getFontHeight(scale);
1338   sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1339   currY += sprStore.getFontHeight(scale);
1341   sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1342   currY += sprStore.getFontHeight(scale);
1344   sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1345   currY += sprStore.getFontHeight(scale);
1347   sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1348   currY += sprStore.getFontHeight(scale);
1350   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1351   currY += sprStore.getFontHeight(scale);
1353   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1354   currY += sprStore.getFontHeight(scale);
1356   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1357   currY += sprStore.getFontHeight(scale);
1359   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1360   currY += sprStore.getFontHeight(scale);
1362   int gs = level.stats.totalGhostSummoned;
1363   sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1364   currY += sprStore.getFontHeight(scale);
1366   currY += sprStore.getFontHeight(scale);
1367   sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1368   currY += sprStore.getFontHeight(scale);
1372 void onDraw () {
1373   if (Video.frameTime == 0) {
1374     onTimePasses();
1375     Video.requestRefresh();
1376   }
1378   if (!level) return;
1380   if (level.framesProcessedFromLastClear < 1) return;
1381   calcMouseMapCoords();
1383   Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1384   Video.clearScreen();
1385   Video.stencil = false;
1386   Video.color = 0xff_ff_ff;
1387   Video.textureFiltering = false;
1388   // don't touch framebuffer alpha
1389   Video.colorMask = Video::CMask.Colors;
1391   Video::ScissorRect scsave;
1392   bool doRestoreGL = false;
1394   /*
1395   if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1396     doRestoreGL = true;
1397     Video.getScissor(scsave);
1398     Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1399     Video.glPushMatrix();
1400     Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1401   }
1402   */
1404   if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1405     doRestoreGL = true;
1406     float scx = float(Video.screenWidth)/float(level.viewWidth);
1407     float scy = float(Video.screenHeight)/float(level.viewHeight);
1408     float scale = fmin(scx, scy);
1409     int calcedW = trunc(level.viewWidth*scale);
1410     int calcedH = trunc(level.viewHeight*scale);
1411     Video.getScissor(scsave);
1412     int ofsx = (Video.screenWidth-calcedW)/2;
1413     int ofsy = (Video.screenHeight-calcedH)/2;
1414     Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1415     Video.glPushMatrix();
1416     Video.glTranslate(ofsx, ofsy);
1417     Video.glScale(scale, scale);
1418   }
1420   //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1421   //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1423   if (fullscreen) {
1424     /*
1425     level.viewOffsetX = 0;
1426     level.viewOffsetY = 0;
1427     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1428     */
1429     /*
1430     float scx = float(Video.screenWidth)/float(level.viewWidth);
1431     float scy = float(Video.screenHeight)/float(level.viewHeight);
1432     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1433     */
1434   }
1437   if (allowRender) {
1438     level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1439   }
1441   if (level.gamePaused && showHelp != 2) {
1442     if (mouseLevelX != int.min) {
1443       int scale = level.global.scale;
1444       if (renderMouseRect) {
1445         Video.color = 0xcf_ff_ff_00;
1446         Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1447       }
1448       if (renderMouseTile) {
1449         Video.color = 0xaf_ff_00_00;
1450         Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1451       }
1452     }
1453   }
1455   switch (doGameSavingPlaying) {
1456     case Replay.Saving:
1457       Video.color = 0x7f_00_ff_00;
1458       sprStore.loadFont('sFont');
1459       sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1460       break;
1461     case Replay.Replaying:
1462       if (level.player && !level.player.dead) {
1463         Video.color = 0x7f_ff_00_00;
1464         sprStore.loadFont('sFont');
1465         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1466         int th = sprStore.getFontHeight(2);
1467         if (replayFastForward) {
1468           sprStore.loadFont('sFontSmall');
1469           string sstr = va("x%d", replayFastForwardSpeed+1);
1470           sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1471         }
1472       }
1473       break;
1474     default:
1475       if (saveGameSession) {
1476         Video.color = 0x7f_ff_7f_00;
1477         sprStore.loadFont('sFont');
1478         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1479       }
1480       break;
1481   }
1484   if (level.player && level.player.dead && !showHelp) {
1485     // darken
1486     Video.color = 0x8f_00_00_00;
1487     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1488     // draw text
1489     if (drawStats) {
1490       drawStatsScreen();
1491     } else {
1492       if (true /*level.inWinCutscene == 0*/) {
1493         Video.color = 0xff_ff_ff;
1494         sprStore.loadFont('sFontSmall');
1495         string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1496                          "\n"~
1497                          "PRESS $PAY TO RESTART GAME\n"~
1498                          "\n"~
1499                          "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1500                          "\n"~
1501                          "TOTAL PLAYING TIME: |%s|"~
1502                          "",
1503                          (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1504                           level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1505                           level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1506                           level.stats.money),
1507                          GameLevel.time2str(level.stats.playingTime)
1508                         );
1509         kmsg = global.expandString(kmsg);
1510         sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1511       }
1512     }
1513   }
1515 #ifdef MASK_TEST
1516   {
1517     Video.color = 0xff_7f_00;
1518     sprStore.loadFont('sFontSmall');
1519     sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1520     auto spf = smask.frames[maskFrame];
1521     sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1522       spf.xofs, spf.yofs,
1523       spf.bx, spf.by, spf.bw, spf.bh,
1524       (spf.maskEmpty ? "TAN" : "ONA"),
1525       (spf.precise ? "TAN" : "ONA")),
1526       2
1527     );
1528     //spf.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1529     //writeln("pos=(", maskSX, ",", maskSY, ")");
1530     int scale = global.config.scale;
1531     int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1532     int mapX = xofs/scale+maskSX;
1533     int mapY = yofs/scale+maskSY;
1534     mapX -= spf.xofs;
1535     mapY -= spf.yofs;
1536     writeln("==== tiles ====");
1537     /*
1538     level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1539       if (t.spectral || !t.isInstanceAlive) return false;
1540       Video.color = 0x7f_ff_00_00;
1541       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);
1542       auto tsf = t.getSpriteFrame();
1544       auto spf = smask.frames[maskFrame];
1545       int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1546       int mapX = xofs/global.config.scale+maskSX;
1547       int mapY = yofs/global.config.scale+maskSY;
1548       mapX -= spf.xofs;
1549       mapY -= spf.yofs;
1550       //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1551       bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1552       writeln("  tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1553       return false;
1554     });
1555     */
1556     level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1557       Video.color = 0x7f_ff_00_00;
1558       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);
1559       return false;
1560     });
1561     //
1562     drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1563     // mask
1564     Video.color = 0xaf_ff_ff_ff;
1565     spf.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1566     Video.color = 0xff_ff_00;
1567     Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1568     // player colbox
1569     {
1570       bool doMirrorSelf;
1571       int fx0, fy0, fx1, fy1;
1572       auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1573       Video.color = 0x7f_00_00_ff;
1574       Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1575     }
1576   }
1577 #endif
1579   if (showHelp) {
1580     Video.color = 0x8f_00_00_00;
1581     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1582     if (optionsPane) {
1583       optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1584     } else {
1585       if (drawStats) {
1586         drawStatsScreen();
1587       } else {
1588         Video.color = 0xff_ff_00;
1589         //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1590         if (showHelp == 1) {
1591           int msx, msy, ww, wh;
1592           Video.getMousePos(out msx, out msy);
1593           Video.getRealWindowSize(out ww, out wh);
1594           if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1595             sprStore.loadFont('sFontSmall');
1596             Video.color = 0xff_ff_00;
1597             sprStore.renderTextWrapped(16, 16, (320-16)*2,
1598               "F1: show this help\n"~
1599               "O : options\n"~
1600               "K : redefine keys\n"~
1601               "I : toggle interpolaion\n"~
1602               "N : create some blood\n"~
1603               "R : generate a new level\n"~
1604               "F : toggle \"Frozen Area\"\n"~
1605               "X : resurrect player\n"~
1606               "Q : teleport to exit\n"~
1607               "D : teleport to damel\n"~
1608               "--------------\n"~
1609               "C : cheat flags menu\n"~
1610               "P : cheat pickup menu\n"~
1611               "E : cheat enemy menu\n"~
1612               "Enter: cheat items menu\n"~
1613               "\n"~
1614               "TAB: toggle 'freeroam' mode\n"~
1615               "",
1616               2);
1617           }
1618         } else {
1619           if (level) level.renderPauseOverlay();
1620         }
1621       }
1622     }
1623     //SoundSystem.UpdateSounds();
1624   }
1625   //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1627   if (doRestoreGL) {
1628     Video.setScissor(scsave);
1629     Video.glPopMatrix();
1630   }
1633   if (TigerEye) {
1634     Video.color = 0xaf_ff_ff_ff;
1635     texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1636   }
1640 // ////////////////////////////////////////////////////////////////////////// //
1641 transient bool gameJustOver;
1642 transient bool waitingForPayRestart;
1645 final void calcMouseMapCoords () {
1646   if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1647     mouseLevelX = int.min;
1648     mouseLevelY = int.min;
1649     return;
1650   }
1651   mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1652   mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1653   //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1657 final void onEvent (ref event_t evt) {
1658   if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1660   if (evt.type == ev_winfocus) {
1661     if (level && !evt.focused) {
1662       level.clearKeys();
1663     }
1664     if (evt.focused) {
1665       //writeln("FOCUS!");
1666       Video.getMousePos(out mouseX, out mouseY);
1667     }
1668     return;
1669   }
1671   if (evt.type == ev_mouse) {
1672     mouseX = evt.x;
1673     mouseY = evt.y;
1674     calcMouseMapCoords();
1675   }
1677   if (evt.type == ev_keydown && evt.keycode == K_F12) {
1678     if (level) toggleFullscreen();
1679     return;
1680   }
1682   if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1683     writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1684     writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1685   }
1687   if (evt.type == ev_keydown) {
1688     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1689     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1690     renderMouseTile = evt.bCtrl;
1691     renderMouseRect = evt.bAlt;
1692   }
1694   if (evt.type == ev_keyup) {
1695     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1696     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1697     renderMouseTile = evt.bCtrl;
1698     renderMouseRect = evt.bAlt;
1699   }
1701   if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1702     int newScale = evt.keycode-48;
1703     if (global.config.scale != newScale) {
1704       global.config.scale = newScale;
1705       if (level) {
1706         level.fixCamera();
1707         cameraTeleportedCB();
1708       }
1709     }
1710     return;
1711   }
1713 #ifdef MASK_TEST
1714   if (evt.type == ev_mouse) {
1715     maskSX = evt.x/global.config.scale;
1716     maskSY = evt.y/global.config.scale;
1717     return;
1718   }
1719   if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1720     maskFrame = max(0, maskFrame-1);
1721     return;
1722   }
1723   if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1724     maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1725     return;
1726   }
1727 #endif
1729   if (showHelp) {
1730     if (optionsPane) {
1731       if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1732         saveCurrentPane();
1733         if (saveOptionsDG) saveOptionsDG();
1734         saveOptionsDG = none;
1735         delete optionsPane;
1736         //SoundSystem.UpdateSounds(); // just in case
1737         if (global.hasSpectacles) level.pickedSpectacles();
1738         return;
1739       }
1740       optionsPane.onEvent(evt);
1741       return;
1742     }
1744     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1745     if (evt.type == ev_keydown) {
1746       if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1747       switch (evt.keycode) {
1748         case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1749         case K_F2: if (showHelp != 2) unpauseGame(); return;
1750         case K_F10: Video.requestQuit(); return;
1751         case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1753         case K_BACKQUOTE:
1754           if (evt.bCtrl) {
1755             allowRender = !allowRender;
1756             unpauseGame();
1757             return;
1758           }
1759           break;
1761         case K_UPARROW: case K_PAD8:
1762           if (drawStats) statsMoveUp();
1763           return;
1765         case K_DOWNARROW: case K_PAD2:
1766           if (drawStats) statsMoveDown();
1767           return;
1769         case K_LEFTARROW: case K_PAD4:
1770           if (level && showHelp == 2 && level.gameShowHelp) {
1771             if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1772           }
1773           return;
1775         case K_RIGHTARROW: case K_PAD6:
1776           if (level && showHelp == 2 && level.gameShowHelp) {
1777             level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1778           }
1779           return;
1781         case K_F6: {
1782           // save level
1783           saveGame("level");
1784           unpauseGame();
1785           return;
1786         }
1788         case K_F9: {
1789           // load level
1790           loadGame("level");
1791           resetFramesAndForceOne();
1792           unpauseGame();
1793           return;
1794         }
1796         case K_F5:
1797           if (/*evt.bCtrl &&*/ showHelp != 2) {
1798             global.plife = 99;
1799             unpauseGame();
1800           }
1801           return;
1803         case K_s:
1804           ++drawStats;
1805           return;
1807         case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1808         case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1809         case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1810         case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1811         case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1812         case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1813         //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1814         //case K_j: global.hasJordans = !global.hasJordans; return;
1815         case K_x:
1816           if (/*evt.bCtrl &&*/ showHelp != 2) {
1817             level.resurrectPlayer();
1818             unpauseGame();
1819           }
1820           return;
1821         case K_r:
1822           //writeln("*** ROOM  SEED: ", global.globalRoomSeed);
1823           //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1824           if (evt.bAlt && level.player && level.player.dead) {
1825             saveGameSession = false;
1826             replayGameSession = true;
1827             unpauseGame();
1828             return;
1829           }
1830           if (/*evt.bCtrl &&*/ showHelp != 2) {
1831             if (evt.bShift) global.idol = false;
1832             level.generateLevel();
1833             level.centerViewAtPlayer();
1834             teleportCameraAt(level.viewStart);
1835             resetFramesAndForceOne();
1836           }
1837           return;
1838         case K_m:
1839           global.toggleMusic();
1840           return;
1841         case K_q:
1842           if (/*evt.bCtrl &&*/ showHelp != 2) {
1843             foreach (MapTile t; level.allExits) {
1844               if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1845                 level.teleportPlayerTo(t.ix+8, t.iy+8);
1846                 unpauseGame();
1847                 return;
1848               }
1849             }
1850           }
1851           return;
1852         case K_d:
1853           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1854             auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1855             if (damsel) {
1856               level.teleportPlayerTo(damsel.ix, damsel.iy);
1857               unpauseGame();
1858             }
1859           }
1860           return;
1861         case K_h:
1862           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1863             MapObject obj;
1864             if (evt.bAlt) {
1865               // locked chest
1866               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1867             } else {
1868               // key
1869               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1870             }
1871             if (obj) {
1872               level.teleportPlayerTo(obj.ix, obj.iy-4);
1873               unpauseGame();
1874             }
1875           }
1876           return;
1877         case K_g:
1878           if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1879             if (level && mouseLevelX != int.min) {
1880               int scale = level.global.scale;
1881               int mapX = mouseLevelX;
1882               int mapY = mouseLevelY;
1883               level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1884             }
1885             return;
1886           }
1887           break;
1888         case K_w:
1889           if (evt.bCtrl && showHelp != 2) {
1890             if (level && mouseLevelX != int.min) {
1891               int scale = level.global.scale;
1892               int mapX = mouseLevelX;
1893               int mapY = mouseLevelY;
1894               level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1895             }
1896             return;
1897           }
1898           break;
1899         case K_a:
1900           if (evt.bCtrl && showHelp != 2) {
1901             if (level && mouseLevelX != int.min) {
1902               int scale = level.global.scale;
1903               int mapX = mouseLevelX;
1904               int mapY = mouseLevelY;
1905               level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1906               level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1907             }
1908             return;
1909           }
1910           break;
1911         case K_b:
1912           if (evt.bCtrl && showHelp != 2) {
1913             if (level && mouseLevelX != int.min) {
1914               int scale = level.global.scale;
1915               int mapX = mouseLevelX;
1916               int mapY = mouseLevelY;
1917               level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1918             }
1919             return;
1920           }
1921           if (evt.bAlt && showHelp != 2) {
1922             if (level && mouseLevelX != int.min) {
1923               int scale = level.global.scale;
1924               int mapX = mouseLevelX;
1925               int mapY = mouseLevelY;
1926               level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1927             }
1928             return;
1929           }
1930           /*
1931           if (evt.bAlt) {
1932             if (level && mouseLevelX != int.min) {
1933               int scale = level.global.scale;
1934               int mapX = mouseLevelX;
1935               int mapY = mouseLevelY;
1936               int wdt = 12;
1937               int hgt = 14;
1938               writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1939               level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1940                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1941                 return false;
1942               });
1943               writeln(" ---");
1944               foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1945                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1946               }
1947             }
1948             return;
1949           }
1950           */
1951           if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1952             auto obj = level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1953           }
1954           return;
1956         case K_DELETE: // suicide
1957           if (doGameSavingPlaying == Replay.None) {
1958             if (level.player && !level.player.dead && evt.bCtrl) {
1959               global.hasAnkh = false;
1960               level.global.plife = 1;
1961               level.player.invincible = 0;
1962               auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1963               if (xplo) xplo.suicide = true;
1964               unpauseGame();
1965             }
1966           }
1967           return;
1969         case K_INSERT:
1970           if (level.player && !level.player.dead && evt.bAlt) {
1971             if (doGameSavingPlaying != Replay.None) {
1972               if (doGameSavingPlaying == Replay.Replaying) {
1973                 stopReplaying();
1974               } else if (doGameSavingPlaying == Replay.Saving) {
1975                 saveGameMovement(dbgSessionMovementFileName, packit:true);
1976               }
1977               doGameSavingPlaying = Replay.None;
1978               stopReplaying();
1979               saveGameSession = false;
1980               replayGameSession = false;
1981               unpauseGame();
1982             }
1983           }
1984           return;
1986         case K_SPACE:
1987           if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1988             level.stats.setMoneyCheat();
1989             level.stats.addMoney(10000);
1990           }
1991           return;
1992       }
1993     }
1994   } else {
1995     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1996       if (level.player && level.player.dead) {
1997         if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1998       } else {
1999         showHelp = 2;
2000         pauseRequested = true;
2001       }
2002       return;
2003     }
2005     if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2006     if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2007     if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2008   }
2010   //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2012   if (level) {
2013     if (!level.player || !level.player.dead) {
2014       gameJustOver = false;
2015     } else if (level.player && level.player.dead) {
2016       if (!gameJustOver) {
2017         drawStats = 0;
2018         gameJustOver = true;
2019         waitingForPayRestart = true;
2020         level.clearKeysPressRelease();
2021         if (doGameSavingPlaying == Replay.None) {
2022           stopReplaying(); // just in case
2023           saveGameStats();
2024         }
2025       }
2026       replayFastForward = false;
2027       if (doGameSavingPlaying == Replay.Saving) {
2028         if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2029         doGameSavingPlaying = Replay.None;
2030         //clearGameMovement();
2031         saveGameSession = false;
2032         replayGameSession = false;
2033       }
2034     }
2035     if (evt.type == ev_keydown || evt.type == ev_keyup) {
2036       bool down = (evt.type == ev_keydown);
2037       if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2038         if (down && evt.keycode == K_f) {
2039           if (evt.bCtrl) {
2040             if (replayFastForwardSpeed != 4) {
2041               replayFastForwardSpeed = 4;
2042               replayFastForward = true;
2043             } else {
2044               replayFastForward = !replayFastForward;
2045             }
2046           } else {
2047             replayFastForwardSpeed = 2;
2048             replayFastForward = !replayFastForward;
2049           }
2050         }
2051       }
2052       if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2053         foreach (int kbidx, int kval; global.config.keybinds) {
2054           if (kval && kval == evt.keycode) {
2055 #ifndef BIGGER_REPLAY_DATA
2056             if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2057 #endif
2058             level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2059           }
2060         }
2061       }
2062       if (level.player && level.player.dead) {
2063         if (down && evt.keycode == K_r && evt.bAlt) {
2064           saveGameSession = false;
2065           replayGameSession = true;
2066           unpauseGame();
2067         }
2068         if (down && evt.keycode == K_s && evt.bAlt) {
2069           bool wasSaveReq = saveGameSession;
2070           stopReplaying(); // just in case
2071           saveGameSession = !wasSaveReq;
2072           replayGameSession = false;
2073           //unpauseGame();
2074         }
2075         if (replayGameSession) {
2076           stopReplaying(); // just in case
2077           saveGameSession = false;
2078           replayGameSession = false;
2079           loadGameMovement(dbgSessionMovementFileName);
2080           loadGame(dbgSessionStateFileName);
2081           doGameSavingPlaying = Replay.Replaying;
2082         } else {
2083           // stats
2084           if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2085           if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2086           if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2087           if (waitingForPayRestart) {
2088             level.isKeyReleased(GameConfig::Key.Pay);
2089             if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2090           } else {
2091             level.isKeyPressed(GameConfig::Key.Pay);
2092             if (level.isKeyReleased(GameConfig::Key.Pay)) {
2093               auto doSave = saveGameSession;
2094               stopReplaying(); // just in case
2095               level.clearKeysPressRelease();
2096               level.restartGame();
2097               level.generateNormalLevel();
2098               if (doSave) {
2099                 saveGameSession = false;
2100                 replayGameSession = false;
2101                 writeln("DBG: saving game session...");
2102                 clearGameMovement();
2103                 doGameSavingPlaying = Replay.Saving;
2104                 saveGame(dbgSessionStateFileName);
2105                 //saveGameMovement(dbgSessionMovementFileName);
2106               }
2107             }
2108           }
2109         }
2110       }
2111     }
2112   }
2116 void levelExited () {
2117   // just in case
2118   saveGameStats();
2122 void initializeVideo () {
2123   Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2124   if (Video.realStencilBits < 8) {
2125     Video.closeScreen();
2126     FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2127   }
2128   /*
2129   if (!loserGPU && !Video.framebufferHasAlpha) {
2130     Video.closeScreen();
2131     FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!\nRun the game with \"--loser-gpu\" arg if you still want to play.");
2132   }
2133   */
2134   if (!Video.framebufferHasAlpha) {
2135     loserGPU = true;
2136     if (level) level.loserGPU = true;
2137   }
2138   /*
2139   if (!Video.glHasNPOT) {
2140     Video.closeScreen();
2141     FatalError("=== YOUR GPU SUX! ===\nno NPOT texture support!");
2142   }
2143   */
2144   if (fullscreen) Video.hideMouseCursor();
2148 void toggleFullscreen () {
2149   Video.showMouseCursor();
2150   Video.closeScreen();
2151   fullscreen = !fullscreen;
2152   initializeVideo();
2156 final void runGameLoop () {
2157   Video.frameTime = 0; // unlimited FPS
2158   lastThinkerTime = 0;
2160   sprStore = SpawnObject(SpriteStore);
2161   sprStore.bDumpLoaded = false;
2163   bgtileStore = SpawnObject(BackTileStore);
2164   bgtileStore.bDumpLoaded = false;
2166   level = SpawnObject(GameLevel);
2167   level.loserGPU = loserGPU;
2168   level.setup(global, sprStore, bgtileStore);
2170   level.BuildYear = BuildYear;
2171   level.BuildMonth = BuildMonth;
2172   level.BuildDay = BuildDay;
2173   level.BuildHour = BuildHour;
2174   level.BuildMin = BuildMin;
2176   level.global = global;
2177   level.sprStore = sprStore;
2178   level.bgtileStore = bgtileStore;
2180   loadGameStats();
2181   //level.stats.introViewed = 0;
2183   if (level.stats.introViewed == 0) {
2184     startMode = StartMode.Intro;
2185     writeln("FORCED INTRO");
2186   } else {
2187     //writeln("INTRO VIWED: ", level.stats.introViewed);
2188     if (level.global.config.skipIntro) startMode = StartMode.Title;
2189   }
2191   level.onBeforeFrame = &beforeNewFrame;
2192   level.onAfterFrame = &afterNewFrame;
2193   level.onInterFrame = &interFrame;
2194   level.onLevelExitedCB = &levelExited;
2195   level.onCameraTeleported = &cameraTeleportedCB;
2197 #ifdef MASK_TEST
2198   maskSX = -0x0ff_fff;
2199   maskSY = maskSX;
2200   smask = sprStore['sExplosionMask'];
2201   maskFrame = 3;
2202 #endif
2204   level.viewWidth = 320*3;
2205   level.viewHeight = 240*3;
2207   Video.swapInterval = (global.config.optVSync ? 1 : 0);
2208   //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2209   fullscreen = global.config.startFullscreen;
2210   initializeVideo();
2212   sprStore.loadFont('sFontSmall');
2214   //SoundSystem.SwapStereo = config.swapStereo;
2215   SoundSystem.NumChannels = 32;
2216   SoundSystem.MaxHearingDistance = 12000;
2217   //SoundSystem.DopplerFactor = 1.0f;
2218   //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2219   SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2220   SoundSystem.ReferenceDistance = 16.0f*4;
2221   SoundSystem.MaxDistance = 16.0f*(5*10);
2223   SoundSystem.Initialize();
2224   if (!SoundSystem.IsInitialized) {
2225     writeln("WARNING: cannot initialize sound system, turning off sound and music");
2226     global.soundDisabled = true;
2227     global.musicDisabled = true;
2228   }
2229   global.fixVolumes();
2231   level.restartGame(); // this will NOT generate a new level
2232   setupCheats();
2233   setupSeeds();
2234   performTimeCheck();
2236   texTigerEye = GLTexture.Load("teye0.png");
2238   if (global.cheatEndGameSequence) {
2239     level.winTime = 12*60+42;
2240     level.stats.money = 6666;
2241     switch (global.cheatEndGameSequence) {
2242       case 1: default: level.startWinCutscene(); break;
2243       case 2: level.startWinCutsceneVolcano(); break;
2244       case 3: level.startWinCutsceneWinFall(); break;
2245     }
2246   } else {
2247     switch (startMode) {
2248       case StartMode.Title: level.restartTitle(); break;
2249       case StartMode.Intro: level.restartIntro(); break;
2250       case StartMode.Stars: level.restartStarsRoom(); break;
2251       case StartMode.Sun: level.restartSunRoom(); break;
2252       case StartMode.Moon: level.restartMoonRoom(); break;
2253       default:
2254         level.generateNormalLevel();
2255         if (startMode == StartMode.Dead) {
2256           level.player.dead = true;
2257           level.player.visible = false;
2258         }
2259         break;
2260     }
2261   }
2263   //global.rope = 666;
2264   //global.bombs = 666;
2266   //global.globalRoomSeed = 871520037;
2267   //global.globalOtherSeed = 1047036290;
2269   //level.createTitleRoom();
2270   //level.createTrans4Room();
2271   //level.createOlmecRoom();
2272   //level.generateLevel();
2274   //level.centerViewAtPlayer();
2275   teleportCameraAt(level.viewStart);
2276   //writeln(Video.swapInterval);
2278   Video.runEventLoop();
2279   Video.showMouseCursor();
2280   Video.closeScreen();
2281   SoundSystem.Shutdown();
2283   if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2284   stopReplaying();
2285   saveGameStats();
2287   delete level;
2291 // ////////////////////////////////////////////////////////////////////////// //
2292 // duplicates are not allowed!
2293 final void checkGameObjNames () {
2294   array!(class!Object) known;
2295   class!Object cc;
2296   int classCount = 0, namedCount = 0;
2297   foreach AllClasses(Object, out cc) {
2298     auto gn = GetClassGameObjName(cc);
2299     if (gn) {
2300       //writeln("'", gn, "' is `", GetClassName(cc), "`");
2301       auto nid = NameToInt(gn);
2302       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));
2303       known[nid] = cc;
2304       ++namedCount;
2305     }
2306     ++classCount;
2307   }
2308   writeln(classCount, " classes, ", namedCount, " game object classes.");
2312 // ////////////////////////////////////////////////////////////////////////// //
2313 #include "timelimit.vc"
2314 //const int TimeLimitDate = 2018232;
2317 void performTimeCheck () {
2318 #ifdef DISABLE_TIME_CHECK
2319 #else
2320 regwegjoi
2321   if (TigerEye) return;
2323   TTimeVal tv;
2324   if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2326   TDateTime tm;
2327   if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2329   int tldate = tm.year*1000+tm.yday;
2331   if (tldate > TimeLimitDate) {
2332     level.maxPlayingTime = 24;
2333   } else {
2334     //writeln("*** days left: ", TimeLimitDate-tldate);
2335   }
2336 #endif
2340 void setupCheats () {
2341   return;
2343   //level.stats.resetTunnelPrices();
2344   startMode = StartMode.Alive;
2345   global.currLevel = 10;
2346   //global.scumGenAlienCraft = true;
2347   //global.scumGenYetiLair = true;
2348   return;
2350   startMode = StartMode.Alive;
2351   global.currLevel = 8;
2352   /*
2353   level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2354   level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2355   level.stats.tunnel1Active = false;
2356   level.stats.tunnel2Active = false;
2357   level.stats.tunnel3Active = false;
2358   */
2359   return;
2361   startMode = StartMode.Alive;
2362   global.currLevel = 2;
2363   global.scumGenShop = true;
2364   //global.scumGenShopType = GameGlobal::ShopType.Craps;
2365   //global.config.scale = 1;
2366   return;
2368   startMode = StartMode.Alive;
2369   global.currLevel = 13;
2370   global.config.scale = 2;
2371   return;
2373   startMode = StartMode.Alive;
2374   global.currLevel = 13;
2375   global.config.scale = 1;
2376   global.cityOfGold = true;
2377   return;
2379   startMode = StartMode.Alive;
2380   global.currLevel = 5;
2381   global.genBlackMarket = true;
2382   return;
2384   startMode = StartMode.Alive;
2385   global.currLevel = 2;
2386   global.scumGenShop = true;
2387   global.scumGenShopType = GameGlobal::ShopType.Weapon;
2388   //global.scumGenShopType = GameGlobal::ShopType.Craps;
2389   //global.config.scale = 1;
2390   return;
2392   //startMode = StartMode.Intro;
2393   //return;
2395   global.currLevel = 2;
2396   startMode = StartMode.Alive;
2397   return;
2399   global.currLevel = 5;
2400   startMode = StartMode.Alive;
2401   global.scumGenLake = true;
2402   global.config.scale = 1;
2403   return;
2405   startMode = StartMode.Alive;
2406   global.cheatCanSkipOlmec = true;
2407   global.currLevel = 16;
2408   //global.currLevel = 5;
2409   //global.currLevel = 13;
2410   //global.config.scale = 1;
2411   return;
2412   //startMode = StartMode.Dead;
2413   //startMode = StartMode.Title;
2414   //startMode = StartMode.Stars;
2415   //startMode = StartMode.Sun;
2416   startMode = StartMode.Moon;
2417   return;
2418   //global.scumGenSacrificePit = true;
2419   //global.scumAlwaysSacrificeAltar = true;
2421   // first lush jungle level
2422   //global.levelType = 1;
2423   /*
2424   global.scumGenCemetary = true;
2425   */
2426   //global.idol = false;
2427   //global.currLevel = 5;
2429   //global.isTunnelMan = true;
2430   //return;
2432   //global.currLevel = 5;
2433   //global.scumGenLake = true;
2435   //global.currLevel = 5;
2436   //global.currLevel = 9;
2437   //global.currLevel = 13;
2438   //global.currLevel = 14;
2439   //global.cheatEndGameSequence = 1;
2440   //return;
2442   //global.currLevel = 6;
2443   global.scumGenAlienCraft = true;
2444   global.currLevel = 9;
2445   //global.scumGenYetiLair = true;
2446   //global.genBlackMarket = true;
2447   //startDead = false;
2448   startMode = StartMode.Alive;
2449   return;
2451   global.cheatCanSkipOlmec = true;
2452   global.currLevel = 15;
2453   startMode = StartMode.Alive;
2454   return;
2456   global.scumGenShop = true;
2457   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2458   global.scumGenShopType = GameGlobal::ShopType.Craps;
2459   //global.scumGenShopType = 6; // craps
2460   //global.scumGenShopType = 7; // kissing
2462   //global.scumAlwaysSacrificeAltar = true;
2466 void setupSeeds () {
2470 // ////////////////////////////////////////////////////////////////////////// //
2471 void main (ref array!string args) {
2472   foreach (string s; args) {
2473     if (s == "--loser-gpu") loserGPU = 1;
2474   }
2476   checkGameObjNames();
2478   appSetName("k8spelunky");
2479   config = SpawnObject(GameConfig);
2480   global = SpawnObject(GameGlobal);
2481   global.config = config;
2482   config.heroType = GameConfig::Hero.Spelunker;
2484   global.randomizeSeedAll();
2486   fillCheatPickupList();
2487   fillCheatItemsList();
2488   fillCheatEnemiesList();
2490   loadGameOptions();
2491   loadKeyboardBindings();
2492   runGameLoop();