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