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