fixed black market generator: if "generate lake" flag is also set, black market will...
[k8vacspelynky.git] / spelunky_main.vc
blob2ed63241023ca3989a8b628a2ad0f41e433e42a2
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       closeVideo();
522       fullscreen = newval;
523       initializeVideo();
524     };
525     auto fsmode = UIIntEnum.Create(pane, &config.fsmode, 1, 2, "FULLSCREEN MODE: ", "YOU CAN CHOOSE EITHER REAL FULLSCREEN MODE, OR SCALED. USUALLY, SCALED WORKS BETTER.");
526     fsmode.names[$] = "REAL";
527     fsmode.names[$] = "SCALED";
528     fsmode.onValueChanged = delegate void (int newval) {
529       if (fullscreen) {
530         closeVideo();
531         initializeVideo();
532       }
533     };
534     auto fsres = UIIntEnum.Create(pane, &config.realfsres, 0, GameConfig::RealFSModes.MAX, "FULLSCREEN RESOLUTION: ", "SELECT RESOLUTION FOR REAL FULLSCREEN MODE.");
535     fsres.names[$] = "1024x768";
536     fsres.names[$] = "1280x960";
537     fsres.names[$] = "1280x1024";
538     fsres.names[$] = "1600x1200";
539     fsres.names[$] = "1680x1050";
540     fsres.names[$] = "1920x1080";
541     fsres.names[$] = "1920x1200";
544   UILabel.Create(pane, "");
545   UILabel.Create(pane, "HUD OPTIONS");
546     UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
547     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.");
548     auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
549     halpha.step = 10;
551     auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
552     ialpha.step = 10;
555   UILabel.Create(pane, "");
556   UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
557     //!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.");
558     //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
559     UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
560     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.");
561     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.");
562     UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
563     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.");
564     UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
567   UILabel.Create(pane, "");
568   UILabel.Create(pane, "GAMEPLAY OPTIONS");
569     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.");
570     UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
571     UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
572     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!");
573     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.");
574     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.");
575     UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
576     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.");
577     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.");
578     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.");
579     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.");
580     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?");
581     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.");
582     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.");
583     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.");
584     UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
585     UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
586     UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
587     auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
588     rstl.names[$] = "RANDOM";
589     rstl.names[$] = "NORMAL";
590     rstl.names[$] = "BIZARRE";
593   UILabel.Create(pane, "");
594   UILabel.Create(pane, "WHIP OPTIONS");
595     UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
596     auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
597     whiptype.names[$] = "NORMAL";
598     whiptype.names[$] = "LONG";
599     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.");
602   UILabel.Create(pane, "");
603   UILabel.Create(pane, "PLAYER OPTIONS");
604     auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
605     herotype.names[$] = "SPELUNKY GUY";
606     herotype.names[$] = "DAMSEL";
607     herotype.names[$] = "TUNNEL MAN";
610   UILabel.Create(pane, "");
611   UILabel.Create(pane, "CHEAT OPTIONS");
612     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.");
613     auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
614     plrlit.names[$] = "NEVER";
615     plrlit.names[$] = "FORCED DARKNESS";
616     plrlit.names[$] = "ALWAYS";
617     UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
618     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'.");
619     rdark.names[$] = "NEVER";
620     rdark.names[$] = "DEFAULT";
621     rdark.names[$] = "ALWAYS";
622     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.");
623     rghost.step = 30;
624     rghost.getNameCB = delegate string (int val) {
625       if (val < 0) return "INSTANT";
626       if (val == 0) return "NEVER";
627       if (val < 120) return va("%d SEC", val);
628       if (val%60 == 0) return va("%d MIN", val/60);
629       if (val%60 == 30) return va("%d.5 MIN", val/60);
630       return va("%d MIN, %d SEC", val/60, val%60);
631     };
632     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.");
634   UILabel.Create(pane, "");
635   UILabel.Create(pane, "CHEAT START OPTIONS");
636     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.");
637     UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
638     UIIntEnum.Create(pane, &config.scumStartLife,  1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
639     UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
640     UIIntEnum.Create(pane, &config.scumStartRope,  1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
643   UILabel.Create(pane, "");
644   UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
645     auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC  : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
646     mm.names[$] = "SILENCE";
647     mm.names[$] = "RESTART";
648     mm.names[$] = "DON'T TOUCH";
650     mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
651     //mm.names[$] = "SILENCE";
652     mm.names[$] = "RESTART";
653     mm.names[$] = "DON'T TOUCH";
656   //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
657   /*
658   swstereo.onValueChanged = delegate void (int newval) {
659     SoundSystem.SwapStereo = newval;
660   };
661   */
663   UILabel.Create(pane, "");
664   UILabel.Create(pane, "SOUND CONTROL CENTER");
665     auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
666     rmusonoff.onValueChanged = delegate void (int newval) {
667       global.restartMusic();
668     };
670     UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
672     auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
673     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
675     rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
676     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
679   saveOptionsDG = delegate void () {
680     writeln("saving options");
681     saveGameOptions();
682   };
683   optionsPaneOfs.x = 42;
684   optionsPaneOfs.y = 0;
686   return pane;
690 final void createBindingsControl (UIPane pane, int keyidx) {
691   string kname, khelp;
692   switch (keyidx) {
693     case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
694     case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
695     case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
696     case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
697     case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
698     case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
699     case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
700     case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
701     case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
702     case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
703     case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
704     default: return;
705   }
706   int arridx = GameConfig.getKeyIndex(keyidx);
707   UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
711 final UIPane createBindingsPane () {
712   UIPane pane = SpawnObject(UIPane);
713   pane.id = 'KeyBindings';
714   pane.sprStore = sprStore;
716   pane.width = 320*3-64;
717   pane.height = 240*3-64;
719   createBindingsControl(pane, GameConfig::Key.Left);
720   createBindingsControl(pane, GameConfig::Key.Right);
721   createBindingsControl(pane, GameConfig::Key.Up);
722   createBindingsControl(pane, GameConfig::Key.Down);
723   createBindingsControl(pane, GameConfig::Key.Jump);
724   createBindingsControl(pane, GameConfig::Key.Run);
725   createBindingsControl(pane, GameConfig::Key.Attack);
726   createBindingsControl(pane, GameConfig::Key.Switch);
727   createBindingsControl(pane, GameConfig::Key.Pay);
728   createBindingsControl(pane, GameConfig::Key.Bomb);
729   createBindingsControl(pane, GameConfig::Key.Rope);
731   saveOptionsDG = delegate void () {
732     writeln("saving keys");
733     saveKeyboardBindings();
734   };
735   optionsPaneOfs.x = 120;
736   optionsPaneOfs.y = 140;
738   return pane;
742 // ////////////////////////////////////////////////////////////////////////// //
743 void clearGameMovement () {
744   debugMovement = SpawnObject(DebugSessionMovement);
745   debugMovement.playconfig = SpawnObject(GameConfig);
746   debugMovement.playconfig.copyGameplayConfigFrom(config);
747   debugMovement.resetReplay();
751 void saveGameMovement (string fname, optional bool packit) {
752   if (debugMovement) appSaveOptions(debugMovement, fname, packit);
753   saveMovementLastTime = GetTickCount();
757 void loadGameMovement (string fname) {
758   delete debugMovement;
759   debugMovement = appLoadOptions(DebugSessionMovement, fname);
760   debugMovement.resetReplay();
761   if (debugMovement) {
762     delete origStats;
763     origStats = level.stats;
764     origStats.global = none;
765     level.stats = SpawnObject(GameStats);
766     level.stats.global = global;
767     delete origConfig;
768     origConfig = config;
769     config = debugMovement.playconfig;
770     global.config = config;
771     global.saveSeeds(origSeeds);
772   }
776 void stopReplaying () {
777   if (debugMovement) {
778     global.restoreSeeds(origSeeds);
779   }
780   delete debugMovement;
781   saveGameSession = false;
782   replayGameSession = false;
783   doGameSavingPlaying = Replay.None;
784   if (origStats) {
785     delete level.stats;
786     origStats.global = global;
787     level.stats = origStats;
788     origStats = none;
789   }
790   if (origConfig) {
791     delete config;
792     config = origConfig;
793     global.config = origConfig;
794     origConfig = none;
795   }
799 // ////////////////////////////////////////////////////////////////////////// //
800 final bool saveGame (string gmname) {
801   return appSaveOptions(level, gmname);
805 final bool loadGame (string gmname) {
806   auto olddel = ImmediateDelete;
807   ImmediateDelete = false;
808   bool res = false;
809   auto stats = level.stats;
810   level.stats = none;
812   auto lvl = appLoadOptions(GameLevel, gmname);
813   if (lvl) {
814     //lvl.global.config = config;
815     delete level;
816     delete global;
818     level = lvl;
819     level.loserGPU = loserGPU;
820     global = level.global;
821     global.config = config;
823     level.sprStore = sprStore;
824     level.bgtileStore = bgtileStore;
827     level.onBeforeFrame = &beforeNewFrame;
828     level.onAfterFrame = &afterNewFrame;
829     level.onInterFrame = &interFrame;
830     level.onLevelExitedCB = &levelExited;
831     level.onCameraTeleported = &cameraTeleportedCB;
833     //level.viewWidth = Video.screenWidth;
834     //level.viewHeight = Video.screenHeight;
835     level.viewWidth = 320*3;
836     level.viewHeight = 240*3;
838     level.onLoaded();
839     level.centerViewAtPlayer();
840     teleportCameraAt(level.viewStart);
842     recalcCameraCoords(0);
844     res = true;
845   }
846   level.stats = stats;
847   level.stats.global = level.global;
849   ImmediateDelete = olddel;
850   CollectGarbage(true); // destroy delayed objects too
851   return res;
855 // ////////////////////////////////////////////////////////////////////////// //
856 float lastThinkerTime;
857 int replaySkipFrame = 0;
860 final void onTimePasses () {
861   float curTime = GetTickCount();
862   if (lastThinkerTime > 0) {
863     if (curTime < lastThinkerTime) {
864       writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
865       lastThinkerTime = curTime;
866       return;
867     }
868     if (replayFastForward && replaySkipFrame) {
869       level.accumTime = 0;
870       lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
871       replaySkipFrame = 0;
872     }
873     level.processThinkers(curTime-lastThinkerTime);
874   }
875   lastThinkerTime = curTime;
879 final void resetFramesAndForceOne () {
880   float curTime = GetTickCount();
881   lastThinkerTime = curTime;
882   level.accumTime = 0;
883   auto wasPaused = level.gamePaused;
884   level.gamePaused = false;
885   if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
886   level.processThinkers(GameLevel::FrameTime);
887   level.gamePaused = wasPaused;
888   //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
892 // ////////////////////////////////////////////////////////////////////////// //
893 private float currFrameDelta; // so level renderer can properly interpolate the player
894 private GameLevel::IVec2D camPrev, camCurr;
895 private GameLevel::IVec2D camShake;
896 private GameLevel::IVec2D viewCameraPos;
899 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
900   camPrev.x = pos.x;
901   camPrev.y = pos.y;
902   camCurr.x = pos.x;
903   camCurr.y = pos.y;
904   viewCameraPos.x = pos.x;
905   viewCameraPos.y = pos.y;
906   camShake.x = 0;
907   camShake.y = 0;
911 // call `recalcCameraCoords()` to get real camera coords after this
912 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
913   // check if camera is moved too far, and teleport it
914   if (doTeleport ||
915       (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
916        abs(camCurr.y-pos.y)/global.scale >= 16*4))
917   {
918     teleportCameraAt(pos);
919   } else {
920     camPrev.x = camCurr.x;
921     camPrev.y = camCurr.y;
922     camCurr.x = pos.x;
923     camCurr.y = pos.y;
924   }
925   camShake.x = level.shakeDir.x*global.scale;
926   camShake.y = level.shakeDir.y*global.scale;
930 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
931   currFrameDelta = frameDelta;
932   viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
933   viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
935   viewCameraPos.x += camShake.x;
936   viewCameraPos.y += camShake.y;
940 GameLevel::SavedKeyState savedKeyState;
942 final void pauseGame () {
943   if (!level.gamePaused) {
944     if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
945     level.gamePaused = true;
946     global.pauseAllSounds();
947   }
951 final void unpauseGame () {
952   if (level.gamePaused) {
953     if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
954     level.gamePaused = false;
955     level.gameShowHelp = false;
956     level.gameHelpScreen = 0;
957     //lastThinkerTime = 0;
958     global.resumeAllSounds();
959   }
960   pauseRequested = false;
961   helpRequested = false;
962   showHelp = false;
966 final void beforeNewFrame (bool frameSkip) {
967   /*
968   if (freeRide) {
969     level.disablePlayerThink = true;
971     int delta = 2;
972     if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
973     if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
974     if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
976     if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
977     if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
978     if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
979     if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
980   } else {
981     level.disablePlayerThink = false;
982     level.fixCamera();
983   }
984   */
985   level.fixCamera();
987   if (!level.gamePaused) {
988     // save seeds for afterframe processing
989     /*
990     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
991       debugMovement.otherSeed = global.globalOtherSeed;
992       debugMovement.roomSeed = global.globalRoomSeed;
993     }
994     */
996     if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
998 #ifdef BIGGER_REPLAY_DATA
999     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
1000       debugMovement.keypresses.length += 1;
1001       level.keysSaveState(debugMovement.keypresses[$-1]);
1002       debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
1003       debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
1004     }
1005 #endif
1007     if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
1008 #ifdef BIGGER_REPLAY_DATA
1009       if (debugMovement.keypos < debugMovement.keypresses.length) {
1010         level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1011         global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1012         global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1013         ++debugMovement.keypos;
1014       }
1015 #else
1016       for (;;) {
1017         int kbidx;
1018         bool down;
1019         auto code = debugMovement.getKey(out kbidx, out down);
1020         if (code == DebugSessionMovement::END_OF_RECORD) {
1021           // do this in main loop, so we can view totals
1022           //stopReplaying();
1023           break;
1024         }
1025         if (code == DebugSessionMovement::END_OF_FRAME) {
1026           break;
1027         }
1028         if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1029         level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1030       }
1031 #endif
1032     }
1033   }
1037 final void afterNewFrame (bool frameSkip) {
1038   if (!replayFastForward) replaySkipFrame = 0;
1040   if (level.gamePaused) return;
1042   if (!level.gamePaused) {
1043     if (doGameSavingPlaying != Replay.None) {
1044       if (doGameSavingPlaying == Replay.Saving) {
1045         replayFastForward = false; // just in case
1046 #ifndef BIGGER_REPLAY_DATA
1047         debugMovement.addEndOfFrame();
1048 #endif
1049         auto stt = GetTickCount();
1050         if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1051       } else if (doGameSavingPlaying == Replay.Replaying) {
1052         if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1053           replaySkipFrame = 1;
1054         }
1055       }
1056     }
1057   }
1059   //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1060   //SoundSystem.UpdateSounds();
1062   //if (!freeRide) level.fixCamera();
1063   setNewCameraPos(level.viewStart);
1064   /*
1065   prevCameraX = currCameraX;
1066   prevCameraY = currCameraY;
1067   currCameraX = level.cameraX;
1068   currCameraY = level.cameraY;
1069   // disable camera interpolation if the screen is shaking
1070   if (level.shakeX|level.shakeY) {
1071     prevCameraX = currCameraX;
1072     prevCameraY = currCameraY;
1073     return;
1074   }
1075   // disable camera interpolation if it moves too far away
1076   if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1077   if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1078   */
1079   recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1081   if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1082     pauseRequested = false;
1083     pauseGame();
1084     if (helpRequested) {
1085       helpRequested = false;
1086       level.gameShowHelp = true;
1087       level.gameHelpScreen = 0;
1088       showHelp = 2;
1089     } else {
1090       if (!showHelp) showHelp = true;
1091     }
1092     writeln("active objects in level: ", level.activeItemsCount);
1093     return;
1094   }
1098 final void interFrame (float frameDelta) {
1099   if (!config.interpolateMovement) return;
1100   recalcCameraCoords(frameDelta);
1104 final void cameraTeleportedCB () {
1105   teleportCameraAt(level.viewStart);
1106   recalcCameraCoords(0);
1110 // ////////////////////////////////////////////////////////////////////////// //
1111 #ifdef MASK_TEST
1112 final void setColorByIdx (bool isset, int col) {
1113   if (col == -666) {
1114     // missed collision: red
1115     Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1116   } else if (col == -999) {
1117     // superfluous collision: blue
1118     Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1119   } else if (col <= 0) {
1120     // no collision: yellow
1121     Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1122   } else if (col > 0) {
1123     // collision: green
1124     Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1125   }
1129 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1130   if (!frm) return;
1131   CollisionMask cm = CollisionMask.Create(frm, false);
1132   if (!cm) return;
1133   int scale = global.config.scale;
1134   int bx0, by0, bx1, by1;
1135   frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1136   Video.color = 0x7f_00_00_ff;
1137   Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1138   if (!cm.isEmptyMask) {
1139     //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1140     foreach (int iy; 0..cm.height) {
1141       foreach (int ix; 0..cm.width) {
1142         int v = cm.mask[ix, iy];
1143         foreach (int dx; 0..32) {
1144           int xx = ix*32+dx;
1145           if (v < 0) {
1146             Video.color = 0x3f_00_ff_00;
1147             Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1148           }
1149           v <<= 1;
1150         }
1151       }
1152     }
1153   } else {
1154     // bounding box
1155     /+
1156     foreach (int iy; 0..frm.tex.height) {
1157       foreach (int ix; 0..(frm.tex.width+31)/31) {
1158         foreach (int dx; 0..32) {
1159           int xx = ix*32+dx;
1160           //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1161           if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1162             setColorByIdx(true, col);
1163             if (col <= 0) Video.color = 0xaf_ff_ff_00;
1164           } else {
1165             Video.color = 0xaf_00_ff_00;
1166           }
1167           Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1168         }
1169       }
1170     }
1171     +/
1172     /*
1173     if (frm.bw > 0 && frm.bh > 0) {
1174       setColorByIdx(true, col);
1175       Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1176       Video.color = 0xff_00_00;
1177       Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1178     }
1179     */
1180   }
1181   delete cm;
1183 #endif
1186 // ////////////////////////////////////////////////////////////////////////// //
1187 transient int drawStats;
1188 transient array!int statsTopItem;
1191 final int totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1192   auto sa = string(a.objName).toUpperCase;
1193   auto sb = string(b.objName).toUpperCase;
1194   if (sa < sb) return -1;
1195   if (sa > sb) return 1;
1196   return 0;
1200 final int getStatsTopItem () {
1201   return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1205 final void setStatsTopItem (int val) {
1206   if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1207   statsTopItem[drawStats] = val;
1211 final void resetStatsTopItem () {
1212   setStatsTopItem(0);
1216 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1217   sprStore.loadFont('sFontSmall');
1218   currX = 64;
1219   currY = 34;
1223 final int calcStatsVisItems () {
1224   int scale = 3;
1225   int currX, currY;
1226   statsDrawGetStartPosLoadFont(currX, currY);
1227   int endY = level.viewHeight-(currY*2);
1228   return max(1, endY/sprStore.getFontHeight(scale));
1232 int getStatsItemCount () {
1233   switch (drawStats) {
1234     case 2: return level.stats.totalKills.length;
1235     case 3: return level.stats.totalDeaths.length;
1236     case 4: return level.stats.totalCollected.length;
1237   }
1238   return -1;
1242 final void statsMoveUp () {
1243   int count = getStatsItemCount();
1244   if (count < 0) return;
1245   int visItems = calcStatsVisItems();
1246   if (count <= visItems) { resetStatsTopItem(); return; }
1247   int top = getStatsTopItem();
1248   if (!top) return;
1249   setStatsTopItem(top-1);
1253 final void statsMoveDown () {
1254   int count = getStatsItemCount();
1255   if (count < 0) return;
1256   int visItems = calcStatsVisItems();
1257   if (count <= visItems) { resetStatsTopItem(); return; }
1258   int top = getStatsTopItem();
1259   //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1260   top = clamp(top+1, 0, count-visItems);
1261   setStatsTopItem(top);
1265 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1266   arr.sort(&totalsNameCmpCB);
1267   int scale = 3;
1269   int currX, currY;
1270   statsDrawGetStartPosLoadFont(currX, currY);
1272   int endY = level.viewHeight-(currY*2);
1273   int visItems = calcStatsVisItems();
1275   if (arr.length <= visItems) resetStatsTopItem();
1277   int topItem = getStatsTopItem();
1279   // "upscroll" mark
1280   if (topItem > 0) {
1281     Video.color = 0x3f_ff_ff_00;
1282     auto spr = sprStore['sPageUp'];
1283     spr.frames[0].blitAt(currX-28, currY, scale);
1284   }
1286   // "downscroll" mark
1287   if (topItem+visItems < arr.length) {
1288     Video.color = 0x3f_ff_ff_00;
1289     auto spr = sprStore['sPageDown'];
1290     spr.frames[0].blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1291   }
1293   Video.color = 0xff_ff_00;
1294   int hiColor = 0x00_ff_00;
1295   int hiColor1 = 0xf_ff_ff;
1297   int it = topItem;
1298   while (it < arr.length && visItems-- > 0) {
1299     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);
1300     currY += sprStore.getFontHeight(scale);
1301     ++it;
1302   }
1306 void drawStatsScreen () {
1307   int deathCount, killCount, collectCount;
1309   sprStore.loadFont('sFontSmall');
1311   Video.color = 0xff_ff_ff;
1312   level.drawTextAtS3Centered(240-2-8, "ESC-RETURN  F10-QUIT  CTRL+DEL-SUICIDE");
1313   level.drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
1315   Video.color = 0xff_ff_00;
1316   int hiColor = 0x00_ff_00;
1318   switch (drawStats) {
1319     case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1320     case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1321     case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1322   }
1324   if (drawStats > 1) {
1325     // turn off
1326     foreach (ref auto i; statsTopItem) i = 0;
1327     drawStats = 0;
1328     return;
1329   }
1331   foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1332   foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1333   foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1335   int currX = 64;
1336   int currY = 96;
1337   int scale = 3;
1339   sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1340   currY += sprStore.getFontHeight(scale);
1342   int gw = level.stats.gamesWon;
1343   sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1344   currY += sprStore.getFontHeight(scale);
1346   sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1347   currY += sprStore.getFontHeight(scale);
1349   sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1350   currY += sprStore.getFontHeight(scale);
1352   sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1353   currY += sprStore.getFontHeight(scale);
1355   sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1356   currY += sprStore.getFontHeight(scale);
1358   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1359   currY += sprStore.getFontHeight(scale);
1361   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1362   currY += sprStore.getFontHeight(scale);
1364   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1365   currY += sprStore.getFontHeight(scale);
1367   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1368   currY += sprStore.getFontHeight(scale);
1370   int gs = level.stats.totalGhostSummoned;
1371   sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1372   currY += sprStore.getFontHeight(scale);
1374   currY += sprStore.getFontHeight(scale);
1375   sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1376   currY += sprStore.getFontHeight(scale);
1380 void onDraw () {
1381   if (Video.frameTime == 0) {
1382     onTimePasses();
1383     Video.requestRefresh();
1384   }
1386   if (!level) return;
1388   if (level.framesProcessedFromLastClear < 1) return;
1389   calcMouseMapCoords();
1391   Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1392   Video.clearScreen();
1393   Video.stencil = false;
1394   Video.color = 0xff_ff_ff;
1395   Video.textureFiltering = false;
1396   // don't touch framebuffer alpha
1397   Video.colorMask = Video::CMask.Colors;
1399   Video::ScissorRect scsave;
1400   bool doRestoreGL = false;
1402   /*
1403   if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1404     doRestoreGL = true;
1405     Video.getScissor(scsave);
1406     Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1407     Video.glPushMatrix();
1408     Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1409   }
1410   */
1412   if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1413     doRestoreGL = true;
1414     float scx = float(Video.screenWidth)/float(level.viewWidth);
1415     float scy = float(Video.screenHeight)/float(level.viewHeight);
1416     float scale = fmin(scx, scy);
1417     int calcedW = trunc(level.viewWidth*scale);
1418     int calcedH = trunc(level.viewHeight*scale);
1419     Video.getScissor(scsave);
1420     int ofsx = (Video.screenWidth-calcedW)/2;
1421     int ofsy = (Video.screenHeight-calcedH)/2;
1422     Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1423     Video.glPushMatrix();
1424     Video.glTranslate(ofsx, ofsy);
1425     Video.glScale(scale, scale);
1426   }
1428   //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1429   //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1431   if (fullscreen) {
1432     /*
1433     level.viewOffsetX = 0;
1434     level.viewOffsetY = 0;
1435     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1436     */
1437     /*
1438     float scx = float(Video.screenWidth)/float(level.viewWidth);
1439     float scy = float(Video.screenHeight)/float(level.viewHeight);
1440     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1441     */
1442   }
1445   if (allowRender) {
1446     level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1447   }
1449   if (level.gamePaused && showHelp != 2) {
1450     if (mouseLevelX != int.min) {
1451       int scale = level.global.scale;
1452       if (renderMouseRect) {
1453         Video.color = 0xcf_ff_ff_00;
1454         Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1455       }
1456       if (renderMouseTile) {
1457         Video.color = 0xaf_ff_00_00;
1458         Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1459       }
1460     }
1461   }
1463   switch (doGameSavingPlaying) {
1464     case Replay.Saving:
1465       Video.color = 0x7f_00_ff_00;
1466       sprStore.loadFont('sFont');
1467       sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1468       break;
1469     case Replay.Replaying:
1470       if (level.player && !level.player.dead) {
1471         Video.color = 0x7f_ff_00_00;
1472         sprStore.loadFont('sFont');
1473         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1474         int th = sprStore.getFontHeight(2);
1475         if (replayFastForward) {
1476           sprStore.loadFont('sFontSmall');
1477           string sstr = va("x%d", replayFastForwardSpeed+1);
1478           sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1479         }
1480       }
1481       break;
1482     default:
1483       if (saveGameSession) {
1484         Video.color = 0x7f_ff_7f_00;
1485         sprStore.loadFont('sFont');
1486         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1487       }
1488       break;
1489   }
1492   if (level.player && level.player.dead && !showHelp) {
1493     // darken
1494     Video.color = 0x8f_00_00_00;
1495     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1496     // draw text
1497     if (drawStats) {
1498       drawStatsScreen();
1499     } else {
1500       if (true /*level.inWinCutscene == 0*/) {
1501         Video.color = 0xff_ff_ff;
1502         sprStore.loadFont('sFontSmall');
1503         string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1504                          "\n"~
1505                          "PRESS $PAY TO RESTART GAME\n"~
1506                          "\n"~
1507                          "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1508                          "\n"~
1509                          "TOTAL PLAYING TIME: |%s|"~
1510                          "",
1511                          (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1512                           level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1513                           level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1514                           level.stats.money),
1515                          GameLevel.time2str(level.stats.playingTime)
1516                         );
1517         kmsg = global.expandString(kmsg);
1518         sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1519       }
1520     }
1521   }
1523 #ifdef MASK_TEST
1524   {
1525     Video.color = 0xff_7f_00;
1526     sprStore.loadFont('sFontSmall');
1527     sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1528     auto spf = smask.frames[maskFrame];
1529     sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1530       spf.xofs, spf.yofs,
1531       spf.bx, spf.by, spf.bw, spf.bh,
1532       (spf.maskEmpty ? "TAN" : "ONA"),
1533       (spf.precise ? "TAN" : "ONA")),
1534       2
1535     );
1536     //spf.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1537     //writeln("pos=(", maskSX, ",", maskSY, ")");
1538     int scale = global.config.scale;
1539     int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1540     int mapX = xofs/scale+maskSX;
1541     int mapY = yofs/scale+maskSY;
1542     mapX -= spf.xofs;
1543     mapY -= spf.yofs;
1544     writeln("==== tiles ====");
1545     /*
1546     level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1547       if (t.spectral || !t.isInstanceAlive) return false;
1548       Video.color = 0x7f_ff_00_00;
1549       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);
1550       auto tsf = t.getSpriteFrame();
1552       auto spf = smask.frames[maskFrame];
1553       int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1554       int mapX = xofs/global.config.scale+maskSX;
1555       int mapY = yofs/global.config.scale+maskSY;
1556       mapX -= spf.xofs;
1557       mapY -= spf.yofs;
1558       //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1559       bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1560       writeln("  tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1561       return false;
1562     });
1563     */
1564     level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1565       Video.color = 0x7f_ff_00_00;
1566       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);
1567       return false;
1568     });
1569     //
1570     drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1571     // mask
1572     Video.color = 0xaf_ff_ff_ff;
1573     spf.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1574     Video.color = 0xff_ff_00;
1575     Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1576     // player colbox
1577     {
1578       bool doMirrorSelf;
1579       int fx0, fy0, fx1, fy1;
1580       auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1581       Video.color = 0x7f_00_00_ff;
1582       Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1583     }
1584   }
1585 #endif
1587   if (showHelp) {
1588     Video.color = 0x8f_00_00_00;
1589     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1590     if (optionsPane) {
1591       optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1592     } else {
1593       if (drawStats) {
1594         drawStatsScreen();
1595       } else {
1596         Video.color = 0xff_ff_00;
1597         //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1598         if (showHelp == 1) {
1599           int msx, msy, ww, wh;
1600           Video.getMousePos(out msx, out msy);
1601           Video.getRealWindowSize(out ww, out wh);
1602           if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1603             sprStore.loadFont('sFontSmall');
1604             Video.color = 0xff_ff_00;
1605             sprStore.renderTextWrapped(16, 16, (320-16)*2,
1606               "F1: show this help\n"~
1607               "O : options\n"~
1608               "K : redefine keys\n"~
1609               "I : toggle interpolaion\n"~
1610               "N : create some blood\n"~
1611               "R : generate a new level\n"~
1612               "F : toggle \"Frozen Area\"\n"~
1613               "X : resurrect player\n"~
1614               "Q : teleport to exit\n"~
1615               "D : teleport to damel\n"~
1616               "--------------\n"~
1617               "C : cheat flags menu\n"~
1618               "P : cheat pickup menu\n"~
1619               "E : cheat enemy menu\n"~
1620               "Enter: cheat items menu\n"~
1621               "\n"~
1622               "TAB: toggle 'freeroam' mode\n"~
1623               "",
1624               2);
1625           }
1626         } else {
1627           if (level) level.renderPauseOverlay();
1628         }
1629       }
1630     }
1631     //SoundSystem.UpdateSounds();
1632   }
1633   //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1635   if (doRestoreGL) {
1636     Video.setScissor(scsave);
1637     Video.glPopMatrix();
1638   }
1641   if (TigerEye) {
1642     Video.color = 0xaf_ff_ff_ff;
1643     texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1644   }
1648 // ////////////////////////////////////////////////////////////////////////// //
1649 transient bool gameJustOver;
1650 transient bool waitingForPayRestart;
1653 final void calcMouseMapCoords () {
1654   if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1655     mouseLevelX = int.min;
1656     mouseLevelY = int.min;
1657     return;
1658   }
1659   mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1660   mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1661   //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1665 final void onEvent (ref event_t evt) {
1666   if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1668   if (evt.type == ev_winfocus) {
1669     if (level && !evt.focused) {
1670       level.clearKeys();
1671     }
1672     if (evt.focused) {
1673       //writeln("FOCUS!");
1674       Video.getMousePos(out mouseX, out mouseY);
1675     }
1676     return;
1677   }
1679   if (evt.type == ev_mouse) {
1680     mouseX = evt.x;
1681     mouseY = evt.y;
1682     calcMouseMapCoords();
1683   }
1685   if (evt.type == ev_keyup && evt.keycode == K_F12) {
1686     if (level) toggleFullscreen();
1687     return;
1688   }
1690   if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1691     writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1692     writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1693   }
1695   if (evt.type == ev_keydown) {
1696     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1697     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1698     renderMouseTile = evt.bCtrl;
1699     renderMouseRect = evt.bAlt;
1700   }
1702   if (evt.type == ev_keyup) {
1703     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1704     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1705     renderMouseTile = evt.bCtrl;
1706     renderMouseRect = evt.bAlt;
1707   }
1709   if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1710     int newScale = evt.keycode-48;
1711     if (global.config.scale != newScale) {
1712       global.config.scale = newScale;
1713       if (level) {
1714         level.fixCamera();
1715         cameraTeleportedCB();
1716       }
1717     }
1718     return;
1719   }
1721 #ifdef MASK_TEST
1722   if (evt.type == ev_mouse) {
1723     maskSX = evt.x/global.config.scale;
1724     maskSY = evt.y/global.config.scale;
1725     return;
1726   }
1727   if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1728     maskFrame = max(0, maskFrame-1);
1729     return;
1730   }
1731   if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1732     maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1733     return;
1734   }
1735 #endif
1737   if (showHelp) {
1738     if (optionsPane) {
1739       if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1740         saveCurrentPane();
1741         if (saveOptionsDG) saveOptionsDG();
1742         saveOptionsDG = none;
1743         delete optionsPane;
1744         //SoundSystem.UpdateSounds(); // just in case
1745         if (global.hasSpectacles) level.pickedSpectacles();
1746         return;
1747       }
1748       optionsPane.onEvent(evt);
1749       return;
1750     }
1752     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1753     if (evt.type == ev_keydown) {
1754       if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1755       switch (evt.keycode) {
1756         case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1757         case K_F2: if (showHelp != 2) unpauseGame(); return;
1758         case K_F10: Video.requestQuit(); return;
1759         case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1761         case K_BACKQUOTE:
1762           if (evt.bCtrl) {
1763             allowRender = !allowRender;
1764             unpauseGame();
1765             return;
1766           }
1767           break;
1769         case K_UPARROW: case K_PAD8:
1770           if (drawStats) statsMoveUp();
1771           return;
1773         case K_DOWNARROW: case K_PAD2:
1774           if (drawStats) statsMoveDown();
1775           return;
1777         case K_LEFTARROW: case K_PAD4:
1778           if (level && showHelp == 2 && level.gameShowHelp) {
1779             if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1780           }
1781           return;
1783         case K_RIGHTARROW: case K_PAD6:
1784           if (level && showHelp == 2 && level.gameShowHelp) {
1785             level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1786           }
1787           return;
1789         case K_F6: {
1790           // save level
1791           saveGame("level");
1792           unpauseGame();
1793           return;
1794         }
1796         case K_F9: {
1797           // load level
1798           loadGame("level");
1799           resetFramesAndForceOne();
1800           unpauseGame();
1801           return;
1802         }
1804         case K_F5:
1805           if (/*evt.bCtrl &&*/ showHelp != 2) {
1806             global.plife = 99;
1807             unpauseGame();
1808           }
1809           return;
1811         case K_s:
1812           ++drawStats;
1813           return;
1815         case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1816         case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1817         case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1818         case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1819         case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1820         case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1821         //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1822         //case K_j: global.hasJordans = !global.hasJordans; return;
1823         case K_x:
1824           if (/*evt.bCtrl &&*/ showHelp != 2) {
1825             level.resurrectPlayer();
1826             unpauseGame();
1827           }
1828           return;
1829         case K_r:
1830           //writeln("*** ROOM  SEED: ", global.globalRoomSeed);
1831           //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1832           if (evt.bAlt && level.player && level.player.dead) {
1833             saveGameSession = false;
1834             replayGameSession = true;
1835             unpauseGame();
1836             return;
1837           }
1838           if (/*evt.bCtrl &&*/ showHelp != 2) {
1839             if (evt.bShift) global.idol = false;
1840             level.generateLevel();
1841             level.centerViewAtPlayer();
1842             teleportCameraAt(level.viewStart);
1843             resetFramesAndForceOne();
1844           }
1845           return;
1846         case K_m:
1847           global.toggleMusic();
1848           return;
1849         case K_q:
1850           if (/*evt.bCtrl &&*/ showHelp != 2) {
1851             foreach (MapTile t; level.allExits) {
1852               if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1853                 level.teleportPlayerTo(t.ix+8, t.iy+8);
1854                 unpauseGame();
1855                 return;
1856               }
1857             }
1858           }
1859           return;
1860         case K_d:
1861           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1862             auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1863             if (damsel) {
1864               level.teleportPlayerTo(damsel.ix, damsel.iy);
1865               unpauseGame();
1866             }
1867           }
1868           return;
1869         case K_h:
1870           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1871             MapObject obj;
1872             if (evt.bAlt) {
1873               // locked chest
1874               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1875             } else {
1876               // key
1877               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1878             }
1879             if (obj) {
1880               level.teleportPlayerTo(obj.ix, obj.iy-4);
1881               unpauseGame();
1882             }
1883           }
1884           return;
1885         case K_g:
1886           if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1887             if (level && mouseLevelX != int.min) {
1888               int scale = level.global.scale;
1889               int mapX = mouseLevelX;
1890               int mapY = mouseLevelY;
1891               level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1892             }
1893             return;
1894           }
1895           break;
1896         case K_w:
1897           if (evt.bCtrl && showHelp != 2) {
1898             if (level && mouseLevelX != int.min) {
1899               int scale = level.global.scale;
1900               int mapX = mouseLevelX;
1901               int mapY = mouseLevelY;
1902               level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1903             }
1904             return;
1905           }
1906           break;
1907         case K_a:
1908           if (evt.bCtrl && showHelp != 2) {
1909             if (level && mouseLevelX != int.min) {
1910               int scale = level.global.scale;
1911               int mapX = mouseLevelX;
1912               int mapY = mouseLevelY;
1913               level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1914               level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1915             }
1916             return;
1917           }
1918           break;
1919         case K_b:
1920           if (evt.bCtrl && showHelp != 2) {
1921             if (level && mouseLevelX != int.min) {
1922               int scale = level.global.scale;
1923               int mapX = mouseLevelX;
1924               int mapY = mouseLevelY;
1925               level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1926             }
1927             return;
1928           }
1929           if (evt.bAlt && showHelp != 2) {
1930             if (level && mouseLevelX != int.min) {
1931               int scale = level.global.scale;
1932               int mapX = mouseLevelX;
1933               int mapY = mouseLevelY;
1934               level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1935             }
1936             return;
1937           }
1938           /*
1939           if (evt.bAlt) {
1940             if (level && mouseLevelX != int.min) {
1941               int scale = level.global.scale;
1942               int mapX = mouseLevelX;
1943               int mapY = mouseLevelY;
1944               int wdt = 12;
1945               int hgt = 14;
1946               writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1947               level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1948                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1949                 return false;
1950               });
1951               writeln(" ---");
1952               foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1953                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1954               }
1955             }
1956             return;
1957           }
1958           */
1959           if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1960             auto obj = level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1961           }
1962           return;
1964         case K_DELETE: // suicide
1965           if (doGameSavingPlaying == Replay.None) {
1966             if (level.player && !level.player.dead && evt.bCtrl) {
1967               global.hasAnkh = false;
1968               level.global.plife = 1;
1969               level.player.invincible = 0;
1970               auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1971               if (xplo) xplo.suicide = true;
1972               unpauseGame();
1973             }
1974           }
1975           return;
1977         case K_INSERT:
1978           if (level.player && !level.player.dead && evt.bAlt) {
1979             if (doGameSavingPlaying != Replay.None) {
1980               if (doGameSavingPlaying == Replay.Replaying) {
1981                 stopReplaying();
1982               } else if (doGameSavingPlaying == Replay.Saving) {
1983                 saveGameMovement(dbgSessionMovementFileName, packit:true);
1984               }
1985               doGameSavingPlaying = Replay.None;
1986               stopReplaying();
1987               saveGameSession = false;
1988               replayGameSession = false;
1989               unpauseGame();
1990             }
1991           }
1992           return;
1994         case K_SPACE:
1995           if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1996             level.stats.setMoneyCheat();
1997             level.stats.addMoney(10000);
1998           }
1999           return;
2000       }
2001     }
2002   } else {
2003     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
2004       if (level.player && level.player.dead) {
2005         if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
2006       } else {
2007         showHelp = 2;
2008         pauseRequested = true;
2009       }
2010       return;
2011     }
2013     if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2014     if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2015     if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2016   }
2018   //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2020   if (level) {
2021     if (!level.player || !level.player.dead) {
2022       gameJustOver = false;
2023     } else if (level.player && level.player.dead) {
2024       if (!gameJustOver) {
2025         drawStats = 0;
2026         gameJustOver = true;
2027         waitingForPayRestart = true;
2028         level.clearKeysPressRelease();
2029         if (doGameSavingPlaying == Replay.None) {
2030           stopReplaying(); // just in case
2031           saveGameStats();
2032         }
2033       }
2034       replayFastForward = false;
2035       if (doGameSavingPlaying == Replay.Saving) {
2036         if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2037         doGameSavingPlaying = Replay.None;
2038         //clearGameMovement();
2039         saveGameSession = false;
2040         replayGameSession = false;
2041       }
2042     }
2043     if (evt.type == ev_keydown || evt.type == ev_keyup) {
2044       bool down = (evt.type == ev_keydown);
2045       if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2046         if (down && evt.keycode == K_f) {
2047           if (evt.bCtrl) {
2048             if (replayFastForwardSpeed != 4) {
2049               replayFastForwardSpeed = 4;
2050               replayFastForward = true;
2051             } else {
2052               replayFastForward = !replayFastForward;
2053             }
2054           } else {
2055             replayFastForwardSpeed = 2;
2056             replayFastForward = !replayFastForward;
2057           }
2058         }
2059       }
2060       if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2061         foreach (int kbidx, int kval; global.config.keybinds) {
2062           if (kval && kval == evt.keycode) {
2063 #ifndef BIGGER_REPLAY_DATA
2064             if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2065 #endif
2066             level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2067           }
2068         }
2069       }
2070       if (level.player && level.player.dead) {
2071         if (down && evt.keycode == K_r && evt.bAlt) {
2072           saveGameSession = false;
2073           replayGameSession = true;
2074           unpauseGame();
2075         }
2076         if (down && evt.keycode == K_s && evt.bAlt) {
2077           bool wasSaveReq = saveGameSession;
2078           stopReplaying(); // just in case
2079           saveGameSession = !wasSaveReq;
2080           replayGameSession = false;
2081           //unpauseGame();
2082         }
2083         if (replayGameSession) {
2084           stopReplaying(); // just in case
2085           saveGameSession = false;
2086           replayGameSession = false;
2087           loadGameMovement(dbgSessionMovementFileName);
2088           loadGame(dbgSessionStateFileName);
2089           doGameSavingPlaying = Replay.Replaying;
2090         } else {
2091           // stats
2092           if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2093           if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2094           if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2095           if (waitingForPayRestart) {
2096             level.isKeyReleased(GameConfig::Key.Pay);
2097             if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2098           } else {
2099             level.isKeyPressed(GameConfig::Key.Pay);
2100             if (level.isKeyReleased(GameConfig::Key.Pay)) {
2101               auto doSave = saveGameSession;
2102               stopReplaying(); // just in case
2103               level.clearKeysPressRelease();
2104               level.restartGame();
2105               level.generateNormalLevel();
2106               if (doSave) {
2107                 saveGameSession = false;
2108                 replayGameSession = false;
2109                 writeln("DBG: saving game session...");
2110                 clearGameMovement();
2111                 doGameSavingPlaying = Replay.Saving;
2112                 saveGame(dbgSessionStateFileName);
2113                 //saveGameMovement(dbgSessionMovementFileName);
2114               }
2115             }
2116           }
2117         }
2118       }
2119     }
2120   }
2124 void levelExited () {
2125   // just in case
2126   saveGameStats();
2130 void closeVideo () {
2131   if (fullscreen && Video.isInitialized) Video.showMouseCursor();
2132   Video.closeScreen();
2136 void initializeVideo () {
2137   int wdt = 320*3;
2138   int hgt = 240*3;
2139   if (fullscreen && global.config.fsmode == 1) {
2140     switch (global.config.realfsres) {
2141       case GameConfig::RealFSModes.VM_1024x768: wdt = 1024; hgt = 768; break;
2142       case GameConfig::RealFSModes.VM_1280x960: wdt = 1280; hgt = 960; break;
2143       case GameConfig::RealFSModes.VM_1280x1024: wdt = 1280; hgt = 1024; break;
2144       case GameConfig::RealFSModes.VM_1600x1200: wdt = 1600; hgt = 1200; break;
2145       case GameConfig::RealFSModes.VM_1680x1050: wdt = 1680; hgt = 1050; break;
2146       case GameConfig::RealFSModes.VM_1920x1080: wdt = 1920; hgt = 1080; break;
2147       case GameConfig::RealFSModes.VM_1920x1200: wdt = 1920; hgt = 1200; break;
2148     }
2149   }
2150   Video.openScreen("Spelunky/VaVoom C", wdt, hgt, (fullscreen ? global.config.fsmode : 0));
2151   if (Video.realStencilBits < 8) {
2152     Video.closeScreen();
2153     FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2154   }
2155   /*
2156   if (!loserGPU && !Video.framebufferHasAlpha) {
2157     Video.closeScreen();
2158     FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!\nRun the game with \"--loser-gpu\" arg if you still want to play.");
2159   }
2160   */
2161   if (!Video.framebufferHasAlpha) {
2162     loserGPU = true;
2163     if (level) level.loserGPU = true;
2164   }
2165   /*
2166   if (!Video.glHasNPOT) {
2167     Video.closeScreen();
2168     FatalError("=== YOUR GPU SUX! ===\nno NPOT texture support!");
2169   }
2170   */
2171   if (fullscreen) Video.hideMouseCursor();
2175 void toggleFullscreen () {
2176   closeVideo();
2177   fullscreen = !fullscreen;
2178   initializeVideo();
2182 final void runGameLoop () {
2183   Video.frameTime = 0; // unlimited FPS
2184   lastThinkerTime = 0;
2186   sprStore = SpawnObject(SpriteStore);
2187   sprStore.bDumpLoaded = false;
2189   bgtileStore = SpawnObject(BackTileStore);
2190   bgtileStore.bDumpLoaded = false;
2192   level = SpawnObject(GameLevel);
2193   level.loserGPU = loserGPU;
2194   level.setup(global, sprStore, bgtileStore);
2196   level.BuildYear = BuildYear;
2197   level.BuildMonth = BuildMonth;
2198   level.BuildDay = BuildDay;
2199   level.BuildHour = BuildHour;
2200   level.BuildMin = BuildMin;
2202   level.global = global;
2203   level.sprStore = sprStore;
2204   level.bgtileStore = bgtileStore;
2206   loadGameStats();
2207   //level.stats.introViewed = 0;
2209   if (level.stats.introViewed == 0) {
2210     startMode = StartMode.Intro;
2211     writeln("FORCED INTRO");
2212   } else {
2213     //writeln("INTRO VIWED: ", level.stats.introViewed);
2214     if (level.global.config.skipIntro) startMode = StartMode.Title;
2215   }
2217   level.onBeforeFrame = &beforeNewFrame;
2218   level.onAfterFrame = &afterNewFrame;
2219   level.onInterFrame = &interFrame;
2220   level.onLevelExitedCB = &levelExited;
2221   level.onCameraTeleported = &cameraTeleportedCB;
2223 #ifdef MASK_TEST
2224   maskSX = -0x0ff_fff;
2225   maskSY = maskSX;
2226   smask = sprStore['sExplosionMask'];
2227   maskFrame = 3;
2228 #endif
2230   level.viewWidth = 320*3;
2231   level.viewHeight = 240*3;
2233   Video.swapInterval = (global.config.optVSync ? 1 : 0);
2234   //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2235   fullscreen = global.config.startFullscreen;
2236   initializeVideo();
2238   sprStore.loadFont('sFontSmall');
2240   //SoundSystem.SwapStereo = config.swapStereo;
2241   SoundSystem.NumChannels = 32;
2242   SoundSystem.MaxHearingDistance = 12000;
2243   //SoundSystem.DopplerFactor = 1.0f;
2244   //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2245   SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2246   SoundSystem.ReferenceDistance = 16.0f*4;
2247   SoundSystem.MaxDistance = 16.0f*(5*10);
2249   SoundSystem.Initialize();
2250   if (!SoundSystem.IsInitialized) {
2251     writeln("WARNING: cannot initialize sound system, turning off sound and music");
2252     global.soundDisabled = true;
2253     global.musicDisabled = true;
2254   }
2255   global.fixVolumes();
2257   level.restartGame(); // this will NOT generate a new level
2258   setupCheats();
2259   setupSeeds();
2260   performTimeCheck();
2262   texTigerEye = GLTexture.Load("teye0.png");
2264   if (global.cheatEndGameSequence) {
2265     level.winTime = 12*60+42;
2266     level.stats.money = 6666;
2267     switch (global.cheatEndGameSequence) {
2268       case 1: default: level.startWinCutscene(); break;
2269       case 2: level.startWinCutsceneVolcano(); break;
2270       case 3: level.startWinCutsceneWinFall(); break;
2271     }
2272   } else {
2273     switch (startMode) {
2274       case StartMode.Title: level.restartTitle(); break;
2275       case StartMode.Intro: level.restartIntro(); break;
2276       case StartMode.Stars: level.restartStarsRoom(); break;
2277       case StartMode.Sun: level.restartSunRoom(); break;
2278       case StartMode.Moon: level.restartMoonRoom(); break;
2279       default:
2280         level.generateNormalLevel();
2281         if (startMode == StartMode.Dead) {
2282           level.player.dead = true;
2283           level.player.visible = false;
2284         }
2285         break;
2286     }
2287   }
2289   //global.rope = 666;
2290   //global.bombs = 666;
2292   //global.globalRoomSeed = 871520037;
2293   //global.globalOtherSeed = 1047036290;
2295   //level.createTitleRoom();
2296   //level.createTrans4Room();
2297   //level.createOlmecRoom();
2298   //level.generateLevel();
2300   //level.centerViewAtPlayer();
2301   teleportCameraAt(level.viewStart);
2302   //writeln(Video.swapInterval);
2304   Video.runEventLoop();
2305   closeVideo();
2306   SoundSystem.Shutdown();
2308   if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2309   stopReplaying();
2310   saveGameStats();
2312   delete level;
2316 // ////////////////////////////////////////////////////////////////////////// //
2317 // duplicates are not allowed!
2318 final void checkGameObjNames () {
2319   array!(class!Object) known;
2320   class!Object cc;
2321   int classCount = 0, namedCount = 0;
2322   foreach AllClasses(Object, out cc) {
2323     auto gn = GetClassGameObjName(cc);
2324     if (gn) {
2325       //writeln("'", gn, "' is `", GetClassName(cc), "`");
2326       auto nid = NameToInt(gn);
2327       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));
2328       known[nid] = cc;
2329       ++namedCount;
2330     }
2331     ++classCount;
2332   }
2333   writeln(classCount, " classes, ", namedCount, " game object classes.");
2337 // ////////////////////////////////////////////////////////////////////////// //
2338 #include "timelimit.vc"
2339 //const int TimeLimitDate = 2018232;
2342 void performTimeCheck () {
2343 #ifdef DISABLE_TIME_CHECK
2344 #else
2345   if (TigerEye) return;
2347   TTimeVal tv;
2348   if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2350   TDateTime tm;
2351   if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2353   int tldate = tm.year*1000+tm.yday;
2355   if (tldate > TimeLimitDate) {
2356     level.maxPlayingTime = 24;
2357   } else {
2358     //writeln("*** days left: ", TimeLimitDate-tldate);
2359   }
2360 #endif
2364 void setupCheats () {
2365   return;
2367   //level.stats.resetTunnelPrices();
2368   startMode = StartMode.Alive;
2369   global.currLevel = 10;
2370   //global.scumGenAlienCraft = true;
2371   //global.scumGenYetiLair = true;
2372   return;
2374   startMode = StartMode.Alive;
2375   global.currLevel = 8;
2376   /*
2377   level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2378   level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2379   level.stats.tunnel1Active = false;
2380   level.stats.tunnel2Active = false;
2381   level.stats.tunnel3Active = false;
2382   */
2383   return;
2385   startMode = StartMode.Alive;
2386   global.currLevel = 2;
2387   global.scumGenShop = true;
2388   //global.scumGenShopType = GameGlobal::ShopType.Craps;
2389   //global.config.scale = 1;
2390   return;
2392   startMode = StartMode.Alive;
2393   global.currLevel = 13;
2394   global.config.scale = 2;
2395   return;
2397   startMode = StartMode.Alive;
2398   global.currLevel = 13;
2399   global.config.scale = 1;
2400   global.cityOfGold = true;
2401   return;
2403   startMode = StartMode.Alive;
2404   global.currLevel = 5;
2405   global.genBlackMarket = true;
2406   return;
2408   startMode = StartMode.Alive;
2409   global.currLevel = 2;
2410   global.scumGenShop = true;
2411   global.scumGenShopType = GameGlobal::ShopType.Weapon;
2412   //global.scumGenShopType = GameGlobal::ShopType.Craps;
2413   //global.config.scale = 1;
2414   return;
2416   //startMode = StartMode.Intro;
2417   //return;
2419   global.currLevel = 2;
2420   startMode = StartMode.Alive;
2421   return;
2423   global.currLevel = 5;
2424   startMode = StartMode.Alive;
2425   global.scumGenLake = true;
2426   global.config.scale = 1;
2427   return;
2429   startMode = StartMode.Alive;
2430   global.cheatCanSkipOlmec = true;
2431   global.currLevel = 16;
2432   //global.currLevel = 5;
2433   //global.currLevel = 13;
2434   //global.config.scale = 1;
2435   return;
2436   //startMode = StartMode.Dead;
2437   //startMode = StartMode.Title;
2438   //startMode = StartMode.Stars;
2439   //startMode = StartMode.Sun;
2440   startMode = StartMode.Moon;
2441   return;
2442   //global.scumGenSacrificePit = true;
2443   //global.scumAlwaysSacrificeAltar = true;
2445   // first lush jungle level
2446   //global.levelType = 1;
2447   /*
2448   global.scumGenCemetary = true;
2449   */
2450   //global.idol = false;
2451   //global.currLevel = 5;
2453   //global.isTunnelMan = true;
2454   //return;
2456   //global.currLevel = 5;
2457   //global.scumGenLake = true;
2459   //global.currLevel = 5;
2460   //global.currLevel = 9;
2461   //global.currLevel = 13;
2462   //global.currLevel = 14;
2463   //global.cheatEndGameSequence = 1;
2464   //return;
2466   //global.currLevel = 6;
2467   global.scumGenAlienCraft = true;
2468   global.currLevel = 9;
2469   //global.scumGenYetiLair = true;
2470   //global.genBlackMarket = true;
2471   //startDead = false;
2472   startMode = StartMode.Alive;
2473   return;
2475   global.cheatCanSkipOlmec = true;
2476   global.currLevel = 15;
2477   startMode = StartMode.Alive;
2478   return;
2480   global.scumGenShop = true;
2481   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2482   global.scumGenShopType = GameGlobal::ShopType.Craps;
2483   //global.scumGenShopType = 6; // craps
2484   //global.scumGenShopType = 7; // kissing
2486   //global.scumAlwaysSacrificeAltar = true;
2490 void setupSeeds () {
2494 // ////////////////////////////////////////////////////////////////////////// //
2495 void main (ref array!string args) {
2496   foreach (string s; args) {
2497     if (s == "--loser-gpu") loserGPU = 1;
2498   }
2500   checkGameObjNames();
2502   appSetName("k8spelunky");
2503   config = SpawnObject(GameConfig);
2504   global = SpawnObject(GameGlobal);
2505   global.config = config;
2506   config.heroType = GameConfig::Hero.Spelunker;
2508   global.randomizeSeedAll();
2510   fillCheatPickupList();
2511   fillCheatItemsList();
2512   fillCheatEnemiesList();
2514   loadGameOptions();
2515   loadKeyboardBindings();
2516   runGameLoop();