don't interpolate camera when it is teleported; also, fix camera position on scale...
[k8vacspelynky.git] / spelunky_main.vc
blob1a48bea469f2f2c8ce21b8256e63f2987b6f064d
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
10  * Spelunky is distributed in the hope that it will be entertaining and useful,
11  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
12  *
13  * The Spelunky User License should be available in "Game Information", which
14  * can be found in the Resource Explorer, or as an external file called COPYING.
15  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
16  *
17  **********************************************************************************/
18 import 'Video';
19 import 'SoundSys';
20 import 'Geom';
21 import 'Game';
22 import 'Generator';
23 import 'Sprites';
25 //#define QUIT_DOUBLE_ESC
27 //#define MASK_TEST
29 //#define BIGGER_REPLAY_DATA
31 // ////////////////////////////////////////////////////////////////////////// //
32 #include "mapent/0all.vc"
33 #include "PlayerPawn.vc"
34 #include "PlayerPowerup.vc"
35 #include "GameLevel.vc"
38 // ////////////////////////////////////////////////////////////////////////// //
39 #include "uisimple.vc"
42 // ////////////////////////////////////////////////////////////////////////// //
43 class DebugSessionMovement : Object;
45 #ifdef BIGGER_REPLAY_DATA
46 array!(GameLevel::SavedKeyState) keypresses;
47 #else
48 array!ubyte keypresses; // on each frame
49 #endif
50 GameConfig playconfig;
52 transient int keypos;
53 transient int otherSeed, roomSeed;
56 override void Destroy () {
57   delete playconfig;
58   keypresses.length = 0;
59   ::Destroy();
63 final void resetReplay () {
64   keypos = 0;
68 #ifndef BIGGER_REPLAY_DATA
69 final void addKey (int kbidx, bool down) {
70   if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
71   keypresses[$] = kbidx|(down ? 0x80 : 0);
75 final void addEndOfFrame () {
76   keypresses[$] = 0xff;
80 enum {
81   NORMAL,
82   END_OF_FRAME,
83   END_OF_RECORD,
86 final int getKey (out int kbidx, out bool down) {
87   if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
88   if (keypos >= keypresses.length) return END_OF_RECORD;
89   ubyte b = keypresses[keypos++];
90   if (b == 0xff) return END_OF_FRAME;
91   kbidx = b&0x7f;
92   down = (b >= 0x80);
93   return NORMAL;
95 #endif
98 // ////////////////////////////////////////////////////////////////////////// //
99 class TempOptionsKeys : Object;
101 int[16*GameConfig::MaxActionBinds] keybinds;
104 // ////////////////////////////////////////////////////////////////////////// //
105 class Main : Object;
107 transient string dbgSessionStateFileName = "debug_game_session_state";
108 transient string dbgSessionMovementFileName = "debug_game_session_movement";
110 GLTexture texTigerEye;
112 GameConfig config;
113 GameGlobal global;
114 SpriteStore sprStore;
115 BackTileStore bgtileStore;
116 GameLevel level;
118 int mouseX = int.min, mouseY = int.min;
119 int mouseLevelX = int.min, mouseLevelY = int.min;
120 bool renderMouseTile;
121 bool renderMouseRect;
123 enum StartMode {
124   Dead,
125   Alive,
126   Title,
127   Stars,
128   Sun,
129   Moon,
132 StartMode startMode = StartMode.Title;
133 bool freeRide = false;
134 bool switchInterpolator;
135 bool pauseRequested;
137 bool replayFastForward = false;
138 int replayFastForwardSpeed = 2;
139 bool saveGameSession = false;
140 bool replayGameSession = false;
141 enum Replay {
142   None,
143   Saving,
144   Replaying,
146 Replay doGameSavingPlaying = Replay.None;
147 float saveMovementLastTime = 0;
148 DebugSessionMovement debugMovement;
149 GameStats origStats; // for replaying
150 GameConfig origConfig; // for replaying
151 int origRoomSeed, origOtherSeed;
153 int showHelp;
154 int escCount;
156 #ifdef MASK_TEST
157 transient int maskSX, maskSY;
158 transient SpriteImage smask;
159 transient int maskFrame;
160 #endif
163 // ////////////////////////////////////////////////////////////////////////// //
164 final void saveKeyboardBindings () {
165   auto tok = SpawnObject(TempOptionsKeys);
166   foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
167   appSaveOptions(tok, "keybindings");
168   delete tok;
172 final void loadKeyboardBindings () {
173   auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
174   if (tok) {
175     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
176     delete tok;
177   }
181 // ////////////////////////////////////////////////////////////////////////// //
182 void saveGameOptions () {
183   appSaveOptions(global.config, "config");
187 void loadGameOptions () {
188   auto cfg = appLoadOptions(GameConfig, "config");
189   if (cfg) {
190     auto oldHero = config.heroType;
191     auto tok = SpawnObject(TempOptionsKeys);
192     foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
193     delete global.config;
194     global.config = cfg;
195     config = cfg;
196     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
197     delete tok;
198     writeln("config loaded");
199     global.restartMusic();
200     global.fixVolumes();
201     //config.heroType = GameConfig::Hero.Spelunker;
202     config.heroType = oldHero;
203   }
204   // fix my bug
205   if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
209 // ////////////////////////////////////////////////////////////////////////// //
210 void saveGameStats () {
211   if (level.stats) appSaveOptions(level.stats, "stats");
215 void loadGameStats () {
216   auto stats = appLoadOptions(GameStats, "stats");
217   if (stats) {
218     delete level.stats;
219     level.stats = stats;
220   }
221   if (!level.stats) level.stats = SpawnObject(GameStats);
222   level.stats.global = global;
226 // ////////////////////////////////////////////////////////////////////////// //
227 struct UIPaneSaveInfo {
228   name id;
229   UIPane::SaveInfo nfo;
232 transient UIPane optionsPane; // either options, or binding editor
234 transient GameLevel::IVec2D optionsPaneOfs;
235 transient void delegate () saveOptionsDG;
237 transient array!UIPaneSaveInfo optionsPaneState;
240 final void saveCurrentPane () {
241   if (!optionsPane || !optionsPane.id) return;
243   // summon ghost
244   if (optionsPane.id == 'CheatFlags') {
245     if (instantGhost && level.ghostTimeLeft > 0) {
246       level.ghostTimeLeft = 1;
247     }
248   }
250   foreach (ref auto psv; optionsPaneState) {
251     if (psv.id == optionsPane.id) {
252       optionsPane.saveState(psv.nfo);
253       return;
254     }
255   }
256   // append new
257   optionsPaneState.length += 1;
258   optionsPaneState[$-1].id = optionsPane.id;
259   optionsPane.saveState(optionsPaneState[$-1].nfo);
263 final void restoreCurrentPane () {
264   if (optionsPane) optionsPane.setupHotkeys(); // why not?
265   if (!optionsPane || !optionsPane.id) return;
266   foreach (ref auto psv; optionsPaneState) {
267     if (psv.id == optionsPane.id) {
268       optionsPane.restoreState(psv.nfo);
269       return;
270     }
271   }
275 // ////////////////////////////////////////////////////////////////////////// //
276 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
277   if (!it.tagClass) return;
278   if (class!MapObject(it.tagClass)) {
279     level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
280     it.owner.closeMe = true;
281   }
285 // ////////////////////////////////////////////////////////////////////////// //
286 transient array!(class!MapObject) cheatItemsList;
289 final void fillCheatItemsList () {
290   cheatItemsList.length = 0;
291   cheatItemsList[$] = ItemProjectileArrow;
292   cheatItemsList[$] = ItemWeaponShotgun;
293   cheatItemsList[$] = ItemWeaponAshShotgun;
294   cheatItemsList[$] = ItemWeaponPistol;
295   cheatItemsList[$] = ItemWeaponMattock;
296   cheatItemsList[$] = ItemWeaponMachete;
297   cheatItemsList[$] = ItemWeaponWebCannon;
298   cheatItemsList[$] = ItemWeaponSceptre;
299   cheatItemsList[$] = ItemWeaponBow;
300   cheatItemsList[$] = ItemBones;
301   cheatItemsList[$] = ItemFakeBones;
302   cheatItemsList[$] = ItemFishBone;
303   cheatItemsList[$] = ItemRock;
304   cheatItemsList[$] = ItemJar;
305   cheatItemsList[$] = ItemSkull;
306   cheatItemsList[$] = ItemGoldenKey;
307   cheatItemsList[$] = ItemGoldIdol;
308   cheatItemsList[$] = ItemCrystalSkull;
309   cheatItemsList[$] = ItemShellSingle;
310   cheatItemsList[$] = ItemChest;
311   cheatItemsList[$] = ItemCrate;
312   cheatItemsList[$] = ItemLockedChest;
313   cheatItemsList[$] = ItemDice;
317 final UIPane createCheatItemsPane () {
318   if (!level.player) return none;
320   UIPane pane = SpawnObject(UIPane);
321   pane.id = 'Items';
322   pane.sprStore = sprStore;
324   pane.width = 320*3-64;
325   pane.height = 240*3-64;
327   foreach (auto ipk; cheatItemsList) {
328     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
329     it.tagClass = ipk;
330   }
332   //optionsPaneOfs.x = 100;
333   //optionsPaneOfs.y = 50;
335   return pane;
339 // ////////////////////////////////////////////////////////////////////////// //
340 transient array!(class!MapObject) cheatEnemiesList;
343 final void fillCheatEnemiesList () {
344   cheatEnemiesList.length = 0;
345   cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
346   cheatEnemiesList[$] = EnemyBat;
347   cheatEnemiesList[$] = EnemySpiderHang;
348   cheatEnemiesList[$] = EnemySpider;
349   cheatEnemiesList[$] = EnemySnake;
350   cheatEnemiesList[$] = EnemyCaveman;
351   cheatEnemiesList[$] = EnemySkeleton;
352   cheatEnemiesList[$] = MonsterShopkeeper;
353   cheatEnemiesList[$] = EnemyZombie;
354   cheatEnemiesList[$] = EnemyVampire;
355   cheatEnemiesList[$] = EnemyFrog;
356   cheatEnemiesList[$] = EnemyGreenFrog;
357   cheatEnemiesList[$] = EnemyFireFrog;
358   cheatEnemiesList[$] = EnemyMantrap;
359   cheatEnemiesList[$] = EnemyScarab;
360   cheatEnemiesList[$] = EnemyFloater;
361   cheatEnemiesList[$] = EnemyBlob;
362   cheatEnemiesList[$] = EnemyMonkey;
363   cheatEnemiesList[$] = EnemyGoldMonkey;
364   cheatEnemiesList[$] = EnemyAlien;
365   cheatEnemiesList[$] = EnemyYeti;
366   cheatEnemiesList[$] = EnemyHawkman;
367   cheatEnemiesList[$] = EnemyUFO;
368   cheatEnemiesList[$] = EnemyYetiKing;
372 final UIPane createCheatEnemiesPane () {
373   if (!level.player) return none;
375   UIPane pane = SpawnObject(UIPane);
376   pane.id = 'Enemies';
377   pane.sprStore = sprStore;
379   pane.width = 320*3-64;
380   pane.height = 240*3-64;
382   foreach (auto ipk; cheatEnemiesList) {
383     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
384     it.tagClass = ipk;
385   }
387   //optionsPaneOfs.x = 100;
388   //optionsPaneOfs.y = 50;
390   return pane;
394 // ////////////////////////////////////////////////////////////////////////// //
395 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
398 final void fillCheatPickupList () {
399   cheatPickupList.length = 0;
400   cheatPickupList[$] = ItemPickupBombBag;
401   cheatPickupList[$] = ItemPickupBombBox;
402   cheatPickupList[$] = ItemPickupPaste;
403   cheatPickupList[$] = ItemPickupRopePile;
404   cheatPickupList[$] = ItemPickupShellBox;
405   cheatPickupList[$] = ItemPickupAnkh;
406   cheatPickupList[$] = ItemPickupCape;
407   cheatPickupList[$] = ItemPickupJetpack;
408   cheatPickupList[$] = ItemPickupUdjatEye;
409   cheatPickupList[$] = ItemPickupCrown;
410   cheatPickupList[$] = ItemPickupKapala;
411   cheatPickupList[$] = ItemPickupParachute;
412   cheatPickupList[$] = ItemPickupCompass;
413   cheatPickupList[$] = ItemPickupSpectacles;
414   cheatPickupList[$] = ItemPickupGloves;
415   cheatPickupList[$] = ItemPickupMitt;
416   cheatPickupList[$] = ItemPickupJordans;
417   cheatPickupList[$] = ItemPickupSpringShoes;
418   cheatPickupList[$] = ItemPickupSpikeShoes;
419   cheatPickupList[$] = ItemPickupTeleporter;
423 final UIPane createCheatPickupsPane () {
424   if (!level.player) return none;
426   UIPane pane = SpawnObject(UIPane);
427   pane.id = 'Pickups';
428   pane.sprStore = sprStore;
430   pane.width = 320*3-64;
431   pane.height = 240*3-64;
433   foreach (auto ipk; cheatPickupList) {
434     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
435     it.tagClass = ipk;
436   }
438   //optionsPaneOfs.x = 100;
439   //optionsPaneOfs.y = 50;
441   return pane;
445 // ////////////////////////////////////////////////////////////////////////// //
446 transient int instantGhost;
448 final UIPane createCheatFlagsPane () {
449   UIPane pane = SpawnObject(UIPane);
450   pane.id = 'CheatFlags';
451   pane.sprStore = sprStore;
453   pane.width = 320*3-64;
454   pane.height = 240*3-64;
456   instantGhost = 0;
458   UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
459   UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
460   UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
461   UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
462   UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
463   //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
464   UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
465   UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
466   UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
467   UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
468   UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
469   UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
470   //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
471   UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
472   UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
473   UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
474   UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
475   UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
477   optionsPaneOfs.x = 100;
478   optionsPaneOfs.y = 50;
480   return pane;
484 final UIPane createOptionsPane () {
485   UIPane pane = SpawnObject(UIPane);
486   pane.id = 'Options';
487   pane.sprStore = sprStore;
489   pane.width = 320*3-64;
490   pane.height = 240*3-64;
493   // this is buggy
494   //!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.");
497   UILabel.Create(pane, "VISUAL OPTIONS");
498     UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
499     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).");
500     // we don't have intro yet
501     //UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
502     UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
505   UILabel.Create(pane, "");
506   UILabel.Create(pane, "HUD OPTIONS");
507     UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
508     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.");
509     auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
510     halpha.step = 10;
512     auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
513     ialpha.step = 10;
516   UILabel.Create(pane, "");
517   UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
518     //!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.");
519     //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
520     UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
521     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.");
522     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.");
523     UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
524     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.");
525     UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
528   UILabel.Create(pane, "");
529   UILabel.Create(pane, "GAMEPLAY OPTIONS");
530     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.");
531     UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
532     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!");
533     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.");
534     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.");
535     UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
536     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.");
537     UICheckBox.Create(pane, &config.ghostRandom, "RANDOM GHOST DELAY", "THIS OPTION WILL RANDOMIZE THE DELAY UNTIL THE GHOST APPEARS AFTER THE TIME LIMIT ABOVE IS REACHED INSTEAD OF USING THE DEFAULT 30 SECONDS. CHANGES EACH LEVEL AND VARIES WITH THE TIME LIMIT YOU SET.");
538     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.");
539     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?");
540     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.");
541     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.");
542     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.");
543     UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
544     UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
545     UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
546     auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
547     rstl.names[$] = "RANDOM";
548     rstl.names[$] = "NORMAL";
549     rstl.names[$] = "BIZARRE";
552   UILabel.Create(pane, "");
553   UILabel.Create(pane, "WHIP OPTIONS");
554     UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
555     auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
556     whiptype.names[$] = "NORMAL";
557     whiptype.names[$] = "LONG";
560   UILabel.Create(pane, "");
561   UILabel.Create(pane, "PLAYER OPTIONS");
562     auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
563     herotype.names[$] = "SPELUNKY GUY";
564     herotype.names[$] = "DAMSEL";
565     herotype.names[$] = "TUNNEL MAN";
568   UILabel.Create(pane, "");
569   UILabel.Create(pane, "CHEAT OPTIONS");
570     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.");
571     auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
572     plrlit.names[$] = "NEVER";
573     plrlit.names[$] = "FORCED DARKNESS";
574     plrlit.names[$] = "ALWAYS";
575     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'.");
576     rdark.names[$] = "NEVER";
577     rdark.names[$] = "DEFAULT";
578     rdark.names[$] = "ALWAYS";
579     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.");
580     rghost.step = 30;
581     rghost.getNameCB = delegate string (int val) {
582       if (val < 0) return "INSTANT";
583       if (val == 0) return "NEVER";
584       if (val < 120) return va("%d SEC", val);
585       if (val%60 == 0) return va("%d MIN", val/60);
586       if (val%60 == 30) return va("%d.5 MIN", val/60);
587       return va("%d MIN, %d SEC", val/60, val%60);
588     };
589     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.");
591   UILabel.Create(pane, "");
592   UILabel.Create(pane, "CHEAT START OPTIONS");
593     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.");
594     UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
595     UIIntEnum.Create(pane, &config.scumStartLife,  1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
596     UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
597     UIIntEnum.Create(pane, &config.scumStartRope,  1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
600   UILabel.Create(pane, "");
601   UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
602     auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC  : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
603     mm.names[$] = "SILENCE";
604     mm.names[$] = "RESTART";
605     mm.names[$] = "DON'T TOUCH";
607     mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
608     //mm.names[$] = "SILENCE";
609     mm.names[$] = "RESTART";
610     mm.names[$] = "DON'T TOUCH";
613   //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
614   /*
615   swstereo.onValueChanged = delegate void (int newval) {
616     SoundSystem.SwapStereo = newval;
617   };
618   */
620   UILabel.Create(pane, "");
621   UILabel.Create(pane, "SOUND CONTROL CENTER");
622     auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
623     rmusonoff.onValueChanged = delegate void (int newval) {
624       global.restartMusic();
625     };
627     UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
629     auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
630     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
632     rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
633     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
636   saveOptionsDG = delegate void () {
637     writeln("saving options");
638     saveGameOptions();
639   };
640   optionsPaneOfs.x = 42;
641   optionsPaneOfs.y = 0;
643   return pane;
647 final void createBindingsControl (UIPane pane, int keyidx) {
648   string kname, khelp;
649   switch (keyidx) {
650     case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
651     case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
652     case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
653     case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
654     case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
655     case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
656     case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
657     case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
658     case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
659     case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
660     case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
661     default: return;
662   }
663   int arridx = GameConfig.getKeyIndex(keyidx);
664   UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
668 final UIPane createBindingsPane () {
669   UIPane pane = SpawnObject(UIPane);
670   pane.id = 'KeyBindings';
671   pane.sprStore = sprStore;
673   pane.width = 320*3-64;
674   pane.height = 240*3-64;
676   createBindingsControl(pane, GameConfig::Key.Left);
677   createBindingsControl(pane, GameConfig::Key.Right);
678   createBindingsControl(pane, GameConfig::Key.Up);
679   createBindingsControl(pane, GameConfig::Key.Down);
680   createBindingsControl(pane, GameConfig::Key.Jump);
681   createBindingsControl(pane, GameConfig::Key.Run);
682   createBindingsControl(pane, GameConfig::Key.Attack);
683   createBindingsControl(pane, GameConfig::Key.Switch);
684   createBindingsControl(pane, GameConfig::Key.Pay);
685   createBindingsControl(pane, GameConfig::Key.Bomb);
686   createBindingsControl(pane, GameConfig::Key.Rope);
688   saveOptionsDG = delegate void () {
689     writeln("saving keys");
690     saveKeyboardBindings();
691   };
692   optionsPaneOfs.x = 120;
693   optionsPaneOfs.y = 140;
695   return pane;
699 // ////////////////////////////////////////////////////////////////////////// //
700 void clearGameMovement () {
701   debugMovement = SpawnObject(DebugSessionMovement);
702   debugMovement.playconfig = SpawnObject(GameConfig);
703   debugMovement.playconfig.copyGameplayConfigFrom(config);
704   debugMovement.resetReplay();
708 void saveGameMovement (string fname) {
709   if (debugMovement) appSaveOptions(debugMovement, fname);
710   saveMovementLastTime = GetTickCount();
714 void loadGameMovement (string fname) {
715   delete debugMovement;
716   debugMovement = appLoadOptions(DebugSessionMovement, fname);
717   debugMovement.resetReplay();
718   if (debugMovement) {
719     delete origStats;
720     origStats = level.stats;
721     origStats.global = none;
722     level.stats = SpawnObject(GameStats);
723     level.stats.global = global;
724     delete origConfig;
725     origConfig = config;
726     config = debugMovement.playconfig;
727     global.config = config;
728     origRoomSeed = global.globalRoomSeed;
729     origOtherSeed = global.globalOtherSeed;
730     writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
731   }
735 void stopReplaying () {
736   if (debugMovement) {
737     writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
738     global.globalRoomSeed = origRoomSeed;
739     global.globalOtherSeed = origOtherSeed;
740   }
741   delete debugMovement;
742   saveGameSession = false;
743   replayGameSession = false;
744   doGameSavingPlaying = Replay.None;
745   if (origStats) {
746     delete level.stats;
747     origStats.global = global;
748     level.stats = origStats;
749     origStats = none;
750   }
751   if (origConfig) {
752     delete config;
753     config = origConfig;
754     global.config = origConfig;
755     origConfig = none;
756   }
760 // ////////////////////////////////////////////////////////////////////////// //
761 final bool saveGame (string gmname) {
762   return appSaveOptions(level, gmname);
766 final bool loadGame (string gmname) {
767   auto olddel = ImmediateDelete;
768   ImmediateDelete = false;
769   bool res = false;
770   auto stats = level.stats;
771   level.stats = none;
773   auto lvl = appLoadOptions(GameLevel, gmname);
774   if (lvl) {
775     //lvl.global.config = config;
776     delete level;
777     delete global;
779     level = lvl;
780     global = level.global;
781     global.config = config;
783     level.sprStore = sprStore;
784     level.bgtileStore = bgtileStore;
787     level.onBeforeFrame = &beforeNewFrame;
788     level.onAfterFrame = &afterNewFrame;
789     level.onInterFrame = &interFrame;
790     level.onLevelExitedCB = &levelExited;
791     level.onCameraTeleported = &cameraTeleportedCB;
793     level.viewWidth = Video.screenWidth;
794     level.viewHeight = Video.screenHeight;
796     level.onLoaded();
797     level.centerViewAtPlayer();
798     teleportCameraAt(level.viewStart);
800     recalcCameraCoords(0);
802     res = true;
803   }
804   level.stats = stats;
805   level.stats.global = level.global;
807   ImmediateDelete = olddel;
808   CollectGarbage(true); // destroy delayed objects too
809   return res;
813 // ////////////////////////////////////////////////////////////////////////// //
814 float lastThinkerTime;
815 int replaySkipFrame = 0;
818 final void onTimePasses () {
819   float curTime = GetTickCount();
820   if (lastThinkerTime > 0) {
821     if (curTime < lastThinkerTime) {
822       writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
823       lastThinkerTime = curTime;
824       return;
825     }
826     if (replayFastForward && replaySkipFrame) {
827       level.accumTime = 0;
828       lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
829       replaySkipFrame = 0;
830     }
831     level.processThinkers(curTime-lastThinkerTime);
832   }
833   lastThinkerTime = curTime;
837 final void resetFramesAndForceOne () {
838   float curTime = GetTickCount();
839   lastThinkerTime = curTime;
840   level.accumTime = 0;
841   auto wasPaused = level.gamePaused;
842   level.gamePaused = false;
843   if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
844   level.processThinkers(GameLevel::FrameTime);
845   level.gamePaused = wasPaused;
846   //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
850 // ////////////////////////////////////////////////////////////////////////// //
851 private float currFrameDelta; // so level renderer can properly interpolate the player
852 private GameLevel::IVec2D camPrev, camCurr;
853 private GameLevel::IVec2D camShake;
854 private GameLevel::IVec2D viewCameraPos;
857 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
858   camPrev.x = pos.x;
859   camPrev.y = pos.y;
860   camCurr.x = pos.x;
861   camCurr.y = pos.y;
862   viewCameraPos.x = pos.x;
863   viewCameraPos.y = pos.y;
864   camShake.x = 0;
865   camShake.y = 0;
869 // call `recalcCameraCoords()` to get real camera coords after this
870 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
871   // check if camera is moved too far, and teleport it
872   if (doTeleport ||
873       (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
874        abs(camCurr.y-pos.y)/global.scale >= 16*4))
875   {
876     teleportCameraAt(pos);
877   } else {
878     camPrev.x = camCurr.x;
879     camPrev.y = camCurr.y;
880     camCurr.x = pos.x;
881     camCurr.y = pos.y;
882   }
883   camShake.x = level.shakeDir.x*global.scale;
884   camShake.y = level.shakeDir.y*global.scale;
888 final void recalcCameraCoords (float frameDelta) {
889   currFrameDelta = frameDelta;
890   viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
891   viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
893   // update sound listener position (it is either at player position, or in viewport center)
894   TVec lv;
895   if (freeRide) {
896     lv = vector(
897       (viewCameraPos.x+level.viewWidth/2.0)/global.scale,
898       (viewCameraPos.y+level.viewHeight/2.0)/global.scale
899     );
900   } else {
901     viewCameraPos.x += camShake.x;
902     viewCameraPos.y += camShake.y;
903     lv = vector(float(level.player.xCenter), float(level.player.yCenter));
904   }
905   SoundSystem.ListenerOrigin = lv;
906   SoundSystem.UpdateSounds();
910 GameLevel::SavedKeyState savedKeyState;
912 final void pauseGame () {
913   if (!level.gamePaused) {
914     if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
915     level.gamePaused = true;
916   }
920 final void unpauseGame () {
921   if (level.gamePaused) {
922     if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
923     level.gamePaused = false;
924     //lastThinkerTime = 0;
925   }
926   showHelp = false;
930 final void beforeNewFrame (bool frameSkip) {
931   if (freeRide) {
932     level.disablePlayerThink = true;
934     int delta = 2;
935     if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
936     if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
937     if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
939     if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
940     if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
941     if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
942     if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
943   } else {
944     level.disablePlayerThink = false;
945   }
947   /*
948   if (level.isKeyDown(PlayerPawn::KeyLeft)) level.player.fltx -= delta;
949   if (level.isKeyDown(PlayerPawn::KeyRight)) level.player.fltx += delta;
950   if (level.isKeyDown(PlayerPawn::KeyUp)) level.player.flty -= delta;
951   if (level.isKeyDown(PlayerPawn::KeyDown)) level.player.flty += delta;
952   */
954   if (!level.gamePaused) {
955     // save seeds for afterframe processing
956     /*
957     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
958       debugMovement.otherSeed = global.globalOtherSeed;
959       debugMovement.roomSeed = global.globalRoomSeed;
960     }
961     */
963     if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
965 #ifdef BIGGER_REPLAY_DATA
966     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
967       debugMovement.keypresses.length += 1;
968       level.keysSaveState(debugMovement.keypresses[$-1]);
969       debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
970       debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
971     }
972 #endif
974     if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
975 #ifdef BIGGER_REPLAY_DATA
976       if (debugMovement.keypos < debugMovement.keypresses.length) {
977         level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
978         global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
979         global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
980         ++debugMovement.keypos;
981       }
982 #else
983       for (;;) {
984         int kbidx;
985         bool down;
986         auto code = debugMovement.getKey(out kbidx, out down);
987         if (code == DebugSessionMovement::END_OF_RECORD) {
988           // do this in main loop, so we can view totals
989           //stopReplaying();
990           break;
991         }
992         if (code == DebugSessionMovement::END_OF_FRAME) {
993           break;
994         }
995         if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
996         level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
997       }
998 #endif
999     }
1000   }
1004 final void afterNewFrame (bool frameSkip) {
1005   if (!replayFastForward) replaySkipFrame = 0;
1007   if (level.gamePaused) return;
1009   if (!level.gamePaused) {
1010     if (doGameSavingPlaying != Replay.None) {
1011       if (doGameSavingPlaying == Replay.Saving) {
1012         replayFastForward = false; // just in case
1013 #ifndef BIGGER_REPLAY_DATA
1014         debugMovement.addEndOfFrame();
1015 #endif
1016         auto stt = GetTickCount();
1017         if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
1018       } else if (doGameSavingPlaying == Replay.Replaying) {
1019         if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1020           replaySkipFrame = 1;
1021         }
1022       }
1023     }
1024   }
1026   //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1027   //SoundSystem.UpdateSounds();
1029   if (!freeRide) level.fixCamera();
1030   setNewCameraPos(level.viewStart);
1031   /*
1032   prevCameraX = currCameraX;
1033   prevCameraY = currCameraY;
1034   currCameraX = level.cameraX;
1035   currCameraY = level.cameraY;
1036   // disable camera interpolation if the screen is shaking
1037   if (level.shakeX|level.shakeY) {
1038     prevCameraX = currCameraX;
1039     prevCameraY = currCameraY;
1040     return;
1041   }
1042   // disable camera interpolation if it moves too far away
1043   if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1044   if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1045   */
1046   if (switchInterpolator) {
1047     switchInterpolator = false;
1048     config.interpolateMovement = !config.interpolateMovement;
1049   }
1050   recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0); // recalc camera coords
1052   if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1053     pauseRequested = false;
1054     pauseGame();
1055     if (!showHelp) showHelp = true;
1056     return;
1057   }
1061 final void interFrame (float frameDelta) {
1062   if (!config.interpolateMovement) return;
1063   recalcCameraCoords(frameDelta);
1067 final void cameraTeleportedCB () {
1068   teleportCameraAt(level.viewStart);
1069   recalcCameraCoords(0);
1073 // ////////////////////////////////////////////////////////////////////////// //
1074 #ifdef MASK_TEST
1075 final void setColorByIdx (bool isset, int col) {
1076   if (col == -666) {
1077     // missed collision: red
1078     Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1079   } else if (col == -999) {
1080     // superfluous collision: blue
1081     Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1082   } else if (col <= 0) {
1083     // no collision: yellow
1084     Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1085   } else if (col > 0) {
1086     // collision: green
1087     Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1088   }
1092 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1093   if (!frm) return;
1094   CollisionMask cm = CollisionMask.Create(frm, false);
1095   if (!cm) return;
1096   int scale = global.config.scale;
1097   int bx0, by0, bx1, by1;
1098   frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1099   Video.color = 0x7f_00_00_ff;
1100   Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1101   if (!cm.isEmptyMask) {
1102     //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1103     foreach (int iy; 0..cm.height) {
1104       foreach (int ix; 0..cm.width) {
1105         int v = cm.mask[ix, iy];
1106         foreach (int dx; 0..32) {
1107           int xx = ix*32+dx;
1108           if (v < 0) {
1109             Video.color = 0x3f_00_ff_00;
1110             Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1111           }
1112           v <<= 1;
1113         }
1114       }
1115     }
1116   } else {
1117     // bounding box
1118     /+
1119     foreach (int iy; 0..frm.tex.height) {
1120       foreach (int ix; 0..(frm.tex.width+31)/31) {
1121         foreach (int dx; 0..32) {
1122           int xx = ix*32+dx;
1123           //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1124           if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1125             setColorByIdx(true, col);
1126             if (col <= 0) Video.color = 0xaf_ff_ff_00;
1127           } else {
1128             Video.color = 0xaf_00_ff_00;
1129           }
1130           Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1131         }
1132       }
1133     }
1134     +/
1135     /*
1136     if (frm.bw > 0 && frm.bh > 0) {
1137       setColorByIdx(true, col);
1138       Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1139       Video.color = 0xff_00_00;
1140       Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1141     }
1142     */
1143   }
1144   delete cm;
1146 #endif
1149 // ////////////////////////////////////////////////////////////////////////// //
1150 transient int drawStats;
1151 transient array!int statsTopItem;
1154 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1155   auto sa = string(a.objName);
1156   auto sb = string(b.objName);
1157   return (sa < sb);
1161 final int getStatsTopItem () {
1162   return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1166 final void setStatsTopItem (int val) {
1167   if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1168   statsTopItem[drawStats] = val;
1172 final void resetStatsTopItem () {
1173   setStatsTopItem(0);
1177 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1178   sprStore.loadFont('sFontSmall');
1179   currX = 64;
1180   currY = 32;
1184 final int calcStatsVisItems () {
1185   int scale = 3;
1186   int currX, currY;
1187   statsDrawGetStartPosLoadFont(currX, currY);
1188   int endY = Video.screenHeight-(currY*2);
1189   return max(1, endY/sprStore.getFontHeight(scale));
1193 int getStatsItemCount () {
1194   switch (drawStats) {
1195     case 2: return level.stats.totalKills.length;
1196     case 3: return level.stats.totalDeaths.length;
1197     case 4: return level.stats.totalCollected.length;
1198   }
1199   return -1;
1203 final void statsMoveUp () {
1204   int count = getStatsItemCount();
1205   if (count < 0) return;
1206   int visItems = calcStatsVisItems();
1207   if (count <= visItems) { resetStatsTopItem(); return; }
1208   int top = getStatsTopItem();
1209   if (!top) return;
1210   setStatsTopItem(top-1);
1214 final void statsMoveDown () {
1215   int count = getStatsItemCount();
1216   if (count < 0) return;
1217   int visItems = calcStatsVisItems();
1218   if (count <= visItems) { resetStatsTopItem(); return; }
1219   int top = getStatsTopItem();
1220   //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1221   top = clamp(top+1, 0, count-visItems);
1222   setStatsTopItem(top);
1226 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1227   GameStats.sortTotalsList(arr, &totalsNameCmpCB);
1228   int scale = 3;
1230   int currX, currY;
1231   statsDrawGetStartPosLoadFont(currX, currY);
1233   int endY = Video.screenHeight-(currY*2);
1234   int visItems = calcStatsVisItems();
1236   if (arr.length <= visItems) resetStatsTopItem();
1238   int topItem = getStatsTopItem();
1240   // "upscroll" mark
1241   if (topItem > 0) {
1242     Video.color = 0x3f_ff_ff_00;
1243     auto spr = sprStore['sPageUp'];
1244     spr.frames[0].tex.blitAt(currX-24, currY, scale);
1245   }
1247   // "downscroll" mark
1248   if (topItem+visItems < arr.length) {
1249     Video.color = 0x3f_ff_ff_00;
1250     auto spr = sprStore['sPageDown'];
1251     spr.frames[0].tex.blitAt(currX-24, endY/*-sprStore.getFontHeight(scale)*/, scale);
1252   }
1254   Video.color = 0xff_ff_00;
1255   int hiColor = 0x00_ff_00;
1256   int hiColor1 = 0xf_ff_ff;
1258   int it = topItem;
1259   while (it < arr.length && visItems-- > 0) {
1260     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);
1261     currY += sprStore.getFontHeight(scale);
1262     ++it;
1263   }
1267 void drawStatsScreen () {
1268   int deathCount, killCount, collectCount;
1270   switch (drawStats) {
1271     case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1272     case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1273     case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1274   }
1275   if (drawStats > 1) {
1276     // turn off
1277     foreach (ref auto i; statsTopItem) i = 0;
1278     drawStats = 0;
1279     return;
1280   }
1282   foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1283   foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1284   foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1286   Video.color = 0xff_ff_00;
1287   int hiColor = 0x00_ff_00;
1288   sprStore.loadFont('sFontSmall');
1290   int currX = 64;
1291   int currY = 96;
1292   int scale = 3;
1294   sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1295   currY += sprStore.getFontHeight(scale);
1297   int gw = level.stats.gamesWon;
1298   sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1299   currY += sprStore.getFontHeight(scale);
1301   sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1302   currY += sprStore.getFontHeight(scale);
1304   sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1305   currY += sprStore.getFontHeight(scale);
1307   sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1308   currY += sprStore.getFontHeight(scale);
1310   sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1311   currY += sprStore.getFontHeight(scale);
1313   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1314   currY += sprStore.getFontHeight(scale);
1316   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1317   currY += sprStore.getFontHeight(scale);
1319   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1320   currY += sprStore.getFontHeight(scale);
1322   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1323   currY += sprStore.getFontHeight(scale);
1325   int gs = level.stats.totalGhostSummoned;
1326   sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1327   currY += sprStore.getFontHeight(scale);
1329   currY += sprStore.getFontHeight(scale);
1330   sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1331   currY += sprStore.getFontHeight(scale);
1335 void onDraw () {
1336   if (Video.frameTime == 0) {
1337     onTimePasses();
1338     Video.requestRefresh();
1339   }
1341   if (!level) return;
1343   if (level.framesProcessedFromLastClear < 1) return;
1344   calcMouseMapCoords();
1346   Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1347   Video.clearScreen();
1348   Video.stencil = false;
1349   Video.color = 0xff_ff_ff;
1350   Video.textureFiltering = false;
1351   // don't touch framebuffer alpha
1352   Video.colorMask = Video::CMask.Colors;
1354   level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1356   if (level.gamePaused) {
1357     if (mouseLevelX != int.min) {
1358       int scale = level.global.scale;
1359       if (renderMouseRect) {
1360         Video.color = 0xcf_ff_ff_00;
1361         Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1362       }
1363       if (renderMouseTile) {
1364         Video.color = 0xaf_ff_00_00;
1365         Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1366       }
1367     }
1368   }
1370   switch (doGameSavingPlaying) {
1371     case Replay.Saving:
1372       Video.color = 0x7f_00_ff_00;
1373       sprStore.loadFont('sFont');
1374       sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1375       break;
1376     case Replay.Replaying:
1377       if (level.player && !level.player.dead) {
1378         Video.color = 0x7f_ff_00_00;
1379         sprStore.loadFont('sFont');
1380         sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1381         int th = sprStore.getFontHeight(2);
1382         if (replayFastForward) {
1383           sprStore.loadFont('sFontSmall');
1384           string sstr = va("x%d", replayFastForwardSpeed+1);
1385           sprStore.renderText(Video.screenWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1386         }
1387       }
1388       break;
1389     default:
1390       if (saveGameSession) {
1391         Video.color = 0x7f_ff_7f_00;
1392         sprStore.loadFont('sFont');
1393         sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1394       }
1395       break;
1396   }
1399   if (level.player && level.player.dead && !showHelp) {
1400     // darken
1401     Video.color = 0x8f_00_00_00;
1402     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1403     // draw text
1404     if (drawStats) {
1405       drawStatsScreen();
1406     } else {
1407       if (true /*level.inWinCutscene == 0*/) {
1408         Video.color = 0xff_ff_ff;
1409         sprStore.loadFont('sFontSmall');
1410         string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1411                          "\n"~
1412                          "PRESS $PAY TO RESTART GAME\n"~
1413                          "\n"~
1414                          "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1415                          "\n"~
1416                          "TOTAL PLAYING TIME: |%s|"~
1417                          "",
1418                          (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1419                           level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1420                           level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1421                           level.stats.money),
1422                          GameLevel.time2str(level.stats.playingTime)
1423                         );
1424         kmsg = global.expandString(kmsg);
1425         sprStore.renderMultilineTextCentered(Video.screenWidth/2, int.min, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1426       }
1427     }
1428   }
1430 #ifdef MASK_TEST
1431   {
1432     Video.color = 0xff_7f_00;
1433     sprStore.loadFont('sFontSmall');
1434     sprStore.renderText(8, Video.screenHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1435     auto spf = smask.frames[maskFrame];
1436     sprStore.renderText(8, Video.screenHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1437       spf.xofs, spf.yofs,
1438       spf.bx, spf.by, spf.bw, spf.bh,
1439       (spf.maskEmpty ? "TAN" : "ONA"),
1440       (spf.precise ? "TAN" : "ONA")),
1441       2
1442     );
1443     //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1444     //writeln("pos=(", maskSX, ",", maskSY, ")");
1445     int scale = global.config.scale;
1446     int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1447     int mapX = xofs/scale+maskSX;
1448     int mapY = yofs/scale+maskSY;
1449     mapX -= spf.xofs;
1450     mapY -= spf.yofs;
1451     writeln("==== tiles ====");
1452     /*
1453     level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1454       if (t.spectral || !t.isInstanceAlive) return false;
1455       Video.color = 0x7f_ff_00_00;
1456       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);
1457       auto tsf = t.getSpriteFrame();
1459       auto spf = smask.frames[maskFrame];
1460       int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1461       int mapX = xofs/global.config.scale+maskSX;
1462       int mapY = yofs/global.config.scale+maskSY;
1463       mapX -= spf.xofs;
1464       mapY -= spf.yofs;
1465       //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1466       bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1467       writeln("  tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1468       return false;
1469     });
1470     */
1471     level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1472       Video.color = 0x7f_ff_00_00;
1473       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);
1474       return false;
1475     });
1476     //
1477     drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1478     // mask
1479     Video.color = 0xaf_ff_ff_ff;
1480     spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1481     Video.color = 0xff_ff_00;
1482     Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1483     // player colbox
1484     {
1485       bool doMirrorSelf;
1486       int fx0, fy0, fx1, fy1;
1487       auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1488       Video.color = 0x7f_00_00_ff;
1489       Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1490     }
1491   }
1492 #endif
1494   if (showHelp) {
1495     Video.color = 0x8f_00_00_00;
1496     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1497     if (optionsPane) {
1498       optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1499     } else {
1500       if (drawStats) {
1501         drawStatsScreen();
1502       } else {
1503         Video.color = 0xff_ff_00;
1504         //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1505         if (showHelp == 1) {
1506           sprStore.loadFont('sFontSmall');
1507           sprStore.renderTextWrapped(16, 16, (320-16)*2,
1508             "F1: show this help\n"~
1509             "O : options\n"~
1510             "K : redefine keys\n"~
1511             "I : toggle interpolaion\n"~
1512             "N : create some blood\n"~
1513             "R : generate a new level\n"~
1514             "F : toggle \"Frozen Area\"\n"~
1515             "X : resurrect player\n"~
1516             "Q : teleport to exit\n"~
1517             "D : teleport to damel\n"~
1518             "--------------\n"~
1519             "C : cheat flags menu\n"~
1520             "P : cheat pickup menu\n"~
1521             "E : cheat enemy menu\n"~
1522             "Enter: cheat items menu\n"~
1523             "\n"~
1524             "TAB: toggle 'freeroam' mode\n"~
1525             "",
1526             2);
1527         }
1528       }
1529     }
1530     //SoundSystem.UpdateSounds();
1531   }
1532   //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1534   if (TigerEye) {
1535     Video.color = 0xaf_ff_ff_ff;
1536     texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1537   }
1541 // ////////////////////////////////////////////////////////////////////////// //
1542 transient bool gameJustOver;
1543 transient bool waitingForPayRestart;
1546 final void calcMouseMapCoords () {
1547   if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1548     mouseLevelX = int.min;
1549     mouseLevelY = int.min;
1550     return;
1551   }
1552   mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1553   mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1554   //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1558 final void onEvent (ref event_t evt) {
1559   if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1561   if (evt.type == ev_winfocus) {
1562     if (level && !evt.focused) {
1563       escCount = 0;
1564       level.clearKeys();
1565     }
1566     if (evt.focused) {
1567       //writeln("FOCUS!");
1568       Video.getMousePos(out mouseX, out mouseY);
1569     }
1570     return;
1571   }
1573   if (evt.type == ev_mouse) {
1574     mouseX = evt.x;
1575     mouseY = evt.y;
1576     calcMouseMapCoords();
1577   }
1579   if (evt.type == ev_keydown) {
1580     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1581     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1582     renderMouseTile = evt.bCtrl;
1583     renderMouseRect = evt.bAlt;
1584   }
1586   if (evt.type == ev_keyup) {
1587     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1588     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1589     renderMouseTile = evt.bCtrl;
1590     renderMouseRect = evt.bAlt;
1591   }
1593   if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1595   if (evt.type == ev_keydown && evt.bAlt && (evt.keycode >= "1" && evt.keycode <= "4")) {
1596     int newScale = evt.keycode-48;
1597     if (global.config.scale != newScale) {
1598       global.config.scale = newScale;
1599       if (level) {
1600         level.fixCamera();
1601         cameraTeleportedCB();
1602       }
1603     }
1604     return;
1605   }
1607 #ifdef MASK_TEST
1608   if (evt.type == ev_mouse) {
1609     maskSX = evt.x/global.config.scale;
1610     maskSY = evt.y/global.config.scale;
1611     return;
1612   }
1613   if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1614     maskFrame = max(0, maskFrame-1);
1615     return;
1616   }
1617   if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1618     maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1619     return;
1620   }
1621 #endif
1623   if (showHelp) {
1624     escCount = 0;
1626     if (optionsPane) {
1627       if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1628         saveCurrentPane();
1629         if (saveOptionsDG) saveOptionsDG();
1630         saveOptionsDG = none;
1631         delete optionsPane;
1632         SoundSystem.UpdateSounds(); // just in case
1633         if (global.hasSpectacles) level.pickedSpectacles();
1634         return;
1635       }
1636       optionsPane.onEvent(evt);
1637       return;
1638     }
1640     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1641     if (evt.type == ev_keydown) {
1642       switch (evt.keycode) {
1643         case K_F1: if (showHelp > 1) showHelp = 1; else unpauseGame(); return;
1644         case K_F10: Video.requestQuit(); return;
1645         case K_F12: showHelp = 3-showHelp; return;
1647         case K_UPARROW: case K_PAD8:
1648           if (drawStats) statsMoveUp();
1649           return;
1650         case K_DOWNARROW: case K_PAD2:
1651           if (drawStats) statsMoveDown();
1652           return;
1654         case K_F6: {
1655           // save level
1656           saveGame("level");
1657           unpauseGame();
1658           return;
1659         }
1661         case K_F9: {
1662           // load level
1663           loadGame("level");
1664           resetFramesAndForceOne();
1665           unpauseGame();
1666           return;
1667         }
1669         case K_F5:
1670           global.plife = 99;
1671           unpauseGame();
1672           return;
1674         case K_s:
1675           ++drawStats;
1676           return;
1678         case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1679         case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1680         case K_c: optionsPane = createCheatFlagsPane(); restoreCurrentPane(); return;
1681         case K_p: optionsPane = createCheatPickupsPane(); restoreCurrentPane(); return;
1682         case K_ENTER: optionsPane = createCheatItemsPane(); restoreCurrentPane(); return;
1683         case K_e: optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); return;
1684         case K_TAB: freeRide = !freeRide; return;
1685         //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1686         //case K_j: global.hasJordans = !global.hasJordans; return;
1687         case K_i: switchInterpolator = true; unpauseGame(); return;
1688         case K_x:
1689           {
1690             /*
1691             auto bomb = ItemBomb(level.MakeMapObject(level.player.ix, level.player.iy, 'oBomb'));
1692             if (bomb) bomb.armIt();
1693             */
1694             level.resurrectPlayer();
1695             unpauseGame();
1696             return;
1697           }
1698         case K_r:
1699           //writeln("*** ROOM  SEED: ", global.globalRoomSeed);
1700           //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1701           if (evt.bAlt && level.player && level.player.dead) {
1702             saveGameSession = false;
1703             replayGameSession = true;
1704             unpauseGame();
1705             return;
1706           }
1707           if (evt.bCtrl) global.idol = false;
1708           level.generateLevel();
1709           level.centerViewAtPlayer();
1710           teleportCameraAt(level.viewStart);
1711           resetFramesAndForceOne();
1712           return;
1713         case K_m:
1714           global.toggleMusic();
1715           return;
1716         case K_v:
1717           level.pickedSpectacles();
1718           return;
1719         case K_f:
1720           global.config.useFrozenRegion = !global.config.useFrozenRegion;
1721           unpauseGame();
1722           return;
1723         case K_q:
1724           if (level.allExits.length) {
1725             level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1726             unpauseGame();
1727           }
1728           return;
1729         case K_d:
1730           if (level.player) {
1731             auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1732             if (damsel) {
1733               level.teleportPlayerTo(damsel.ix, damsel.iy);
1734               unpauseGame();
1735             }
1736           }
1737           return;
1738         case K_h:
1739           if (level.player) {
1740             auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1741             if (obj) {
1742               level.teleportPlayerTo(obj.ix, obj.iy-4);
1743               unpauseGame();
1744             }
1745           }
1746           return;
1747         case K_j:
1748           if (level.player) {
1749             auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1750             if (obj) {
1751               level.teleportPlayerTo(obj.ix, obj.iy);
1752               unpauseGame();
1753             }
1754           }
1755           return;
1756         case K_b:
1757           if (evt.bCtrl) {
1758             if (level && mouseLevelX != int.min) {
1759               int scale = level.global.scale;
1760               int mapX = mouseLevelX;
1761               int mapY = mouseLevelY;
1762               level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1763             }
1764             return;
1765           }
1766           if (evt.bAlt) {
1767             if (level && mouseLevelX != int.min) {
1768               int scale = level.global.scale;
1769               int mapX = mouseLevelX;
1770               int mapY = mouseLevelY;
1771               int wdt = 12;
1772               int hgt = 14;
1773               writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1774               level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1775                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1776                 return false;
1777               });
1778               writeln(" ---");
1779               foreach (MapTile t; level.miscTileGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false)) {
1780                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1781               }
1782             }
1783             return;
1784           }
1785           {
1786             auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1787             //if (obj) obj.monkey = monkey;
1788             if (obj) {
1789               //playSound('sndThump');
1790               unpauseGame();
1791             }
1792           }
1793           return;
1795         case K_DELETE: // suicide
1796           if (doGameSavingPlaying == Replay.None) {
1797             if (level.player && !level.player.dead && evt.bCtrl) {
1798               global.hasAnkh = false;
1799               level.global.plife = 1;
1800               level.player.invincible = 0;
1801               level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion');
1802               unpauseGame();
1803             }
1804           }
1805           return;
1807         case K_INSERT:
1808           if (level.player && !level.player.dead && evt.bAlt) {
1809             if (doGameSavingPlaying != Replay.None) {
1810               if (doGameSavingPlaying == Replay.Replaying) {
1811                 stopReplaying();
1812               } else if (doGameSavingPlaying == Replay.Saving) {
1813                 saveGameMovement(dbgSessionMovementFileName);
1814               }
1815               doGameSavingPlaying = Replay.None;
1816               stopReplaying();
1817               saveGameSession = false;
1818               replayGameSession = false;
1819               unpauseGame();
1820             }
1821           }
1822           return;
1824         case K_SPACE:
1825           level.stats.setMoneyCheat();
1826           level.stats.addMoney(10000);
1827           return;
1828       }
1829     }
1830   } else {
1831     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1832       if (level.player && level.player.dead) {
1833         //Video.requestQuit();
1834         escCount = 0;
1835         if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1836       } else {
1837 #ifdef QUIT_DOUBLE_ESC
1838         if (++escCount == 2) Video.requestQuit();
1839 #else
1840         showHelp = 2;
1841         pauseRequested = true;
1842 #endif
1843       }
1844       return;
1845     }
1846     if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; return; }
1847   }
1849   if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1851   if (level) {
1852     if (!level.player || !level.player.dead) {
1853       gameJustOver = false;
1854     } else if (level.player && level.player.dead) {
1855       if (!gameJustOver) {
1856         drawStats = 0;
1857         gameJustOver = true;
1858         waitingForPayRestart = true;
1859         level.clearKeysPressRelease();
1860         if (doGameSavingPlaying == Replay.None) {
1861           stopReplaying(); // just in case
1862           saveGameStats();
1863         }
1864       }
1865       replayFastForward = false;
1866       if (doGameSavingPlaying == Replay.Saving) {
1867         if (debugMovement) saveGameMovement(dbgSessionMovementFileName);
1868         doGameSavingPlaying = Replay.None;
1869         //clearGameMovement();
1870         saveGameSession = false;
1871         replayGameSession = false;
1872       }
1873     }
1874     if (evt.type == ev_keydown || evt.type == ev_keyup) {
1875       bool down = (evt.type == ev_keydown);
1876       if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
1877         if (down && evt.keycode == K_f) {
1878           if (evt.bCtrl) {
1879             if (replayFastForwardSpeed != 4) {
1880               replayFastForwardSpeed = 4;
1881               replayFastForward = true;
1882             } else {
1883               replayFastForward = !replayFastForward;
1884             }
1885           } else {
1886             replayFastForwardSpeed = 2;
1887             replayFastForward = !replayFastForward;
1888           }
1889         }
1890       }
1891       if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
1892         foreach (int kbidx, int kval; global.config.keybinds) {
1893           if (kval && kval == evt.keycode) {
1894 #ifndef BIGGER_REPLAY_DATA
1895             if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
1896 #endif
1897             level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1898           }
1899         }
1900       }
1901       if (level.player && level.player.dead) {
1902         if (down && evt.keycode == K_r && evt.bAlt) {
1903           saveGameSession = false;
1904           replayGameSession = true;
1905           unpauseGame();
1906         }
1907         if (down && evt.keycode == K_s && evt.bAlt) {
1908           bool wasSaveReq = saveGameSession;
1909           stopReplaying(); // just in case
1910           saveGameSession = !wasSaveReq;
1911           replayGameSession = false;
1912           //unpauseGame();
1913         }
1914         if (replayGameSession) {
1915           stopReplaying(); // just in case
1916           saveGameSession = false;
1917           replayGameSession = false;
1918           loadGameMovement(dbgSessionMovementFileName);
1919           loadGame(dbgSessionStateFileName);
1920           doGameSavingPlaying = Replay.Replaying;
1921         } else {
1922           if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
1923           if (waitingForPayRestart) {
1924             level.isKeyReleased(GameConfig::Key.Pay);
1925             if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
1926           } else {
1927             level.isKeyPressed(GameConfig::Key.Pay);
1928             if (level.isKeyReleased(GameConfig::Key.Pay)) {
1929               auto doSave = saveGameSession;
1930               stopReplaying(); // just in case
1931               level.clearKeysPressRelease();
1932               level.restartGame();
1933               level.generateNormalLevel();
1934               if (doSave) {
1935                 saveGameSession = false;
1936                 replayGameSession = false;
1937                 writeln("DBG: saving game session...");
1938                 clearGameMovement();
1939                 doGameSavingPlaying = Replay.Saving;
1940                 saveGame(dbgSessionStateFileName);
1941                 saveGameMovement(dbgSessionMovementFileName);
1942               }
1943             }
1944           }
1945         }
1946       }
1947     }
1948   }
1952 void levelExited () {
1953   // just in case
1954   saveGameStats();
1958 final void runGameLoop () {
1959   Video.frameTime = 0; // unlimited FPS
1960   lastThinkerTime = 0;
1962   sprStore = SpawnObject(SpriteStore);
1963   sprStore.bDumpLoaded = false;
1965   bgtileStore = SpawnObject(BackTileStore);
1966   bgtileStore.bDumpLoaded = false;
1968   level = SpawnObject(GameLevel);
1969   level.setup(global, sprStore, bgtileStore);
1971   level.global = global;
1972   level.sprStore = sprStore;
1973   level.bgtileStore = bgtileStore;
1975   loadGameStats();
1977   level.onBeforeFrame = &beforeNewFrame;
1978   level.onAfterFrame = &afterNewFrame;
1979   level.onInterFrame = &interFrame;
1980   level.onLevelExitedCB = &levelExited;
1981   level.onCameraTeleported = &cameraTeleportedCB;
1983 #ifdef MASK_TEST
1984   maskSX = -0x0ff_fff;
1985   maskSY = maskSX;
1986   smask = sprStore['sExplosionMask'];
1987   maskFrame = 3;
1988 #endif
1990   sprStore.loadFont('sFontSmall');
1992   Video.swapInterval = (global.config.optVSync ? 1 : 0);
1993   Video.openScreen("Spelunky/VaVoom C", 320*3, 240*3);
1995   if (Video.realStencilBits < 8) {
1996     Video.closeScreen();
1997     FatalError("FATAL: no stencil buffer!");
1998   }
1999   if (!Video.framebufferHasAlpha) {
2000     Video.closeScreen();
2001     FatalError("FATAL: no alpha channel in framebuffer!");
2002   }
2004   //SoundSystem.SwapStereo = config.swapStereo;
2005   SoundSystem.DopplerFactor = 1.0f;
2006   SoundSystem.DopplerVelocity = 10000.0f;
2007   SoundSystem.RolloffFactor = 1.0f;
2008   //SoundSystem.ReferenceDistance = 32.0f; // The distance under which the volume for the source would normally drop by half (before being influenced by rolloff factor or AL_MAX_DISTANCE)
2009   SoundSystem.ReferenceDistance = 32.0f*5; // The distance under which the volume for the source would normally drop by half (before being influenced by rolloff factor or AL_MAX_DISTANCE)
2010   SoundSystem.MaxDistance = 800.0f*2; // Used with the Inverse Clamped Distance Model to set the distance where there will no longer be any attenuation of the source
2011   SoundSystem.NumChannels = 64;
2012   SoundSystem.Sound2DPos = vector(0, 0, -1);
2014   SoundSystem.Initialize();
2015   global.fixVolumes();
2017   level.viewWidth = Video.screenWidth;
2018   level.viewHeight = Video.screenHeight;
2020   level.restartGame(); // this will NOT generate a new level
2021   setupCheats();
2022   setupSeeds();
2023   performTimeCheck();
2025   texTigerEye = GLTexture.Load("sprites/teye0.png");
2027   if (global.cheatEndGameSequence) {
2028     level.winTime = 12*60+42;
2029     level.stats.money = 6666;
2030     switch (global.cheatEndGameSequence) {
2031       case 1: default: level.startWinCutscene(); break;
2032       case 2: level.startWinCutsceneVolcano(); break;
2033       case 3: level.startWinCutsceneWinFall(); break;
2034     }
2035   } else {
2036     switch (startMode) {
2037       case StartMode.Title: level.restartTitle(); break;
2038       case StartMode.Stars: level.restartStarsRoom(); break;
2039       case StartMode.Sun: level.restartSunRoom(); break;
2040       case StartMode.Moon: level.restartMoonRoom(); break;
2041       default:
2042         level.generateNormalLevel();
2043         if (startMode == StartMode.Dead) {
2044           level.player.dead = true;
2045           level.player.visible = false;
2046         }
2047         break;
2048     }
2049   }
2051   //global.rope = 666;
2052   //global.bombs = 666;
2054   //global.globalRoomSeed = 871520037;
2055   //global.globalOtherSeed = 1047036290;
2057   //level.createTitleRoom();
2058   //level.createTrans4Room();
2059   //level.createOlmecRoom();
2060   //level.generateLevel();
2062   //level.centerViewAtPlayer();
2063   teleportCameraAt(level.viewStart);
2064   //writeln(Video.swapInterval);
2066   Video.runEventLoop();
2067   Video.closeScreen();
2069   if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName);
2070   stopReplaying();
2071   saveGameStats();
2073   level.clearTiles();
2074   level.clearObjects();
2078 // ////////////////////////////////////////////////////////////////////////// //
2079 // duplicates are not allowed!
2080 final void checkGameObjNames () {
2081   array!(class!Object) known;
2082   class!Object cc;
2083   int classCount = 0, namedCount = 0;
2084   foreach AllClasses(Object, out cc) {
2085     auto gn = GetClassGameObjName(cc);
2086     if (gn) {
2087       //writeln("'", gn, "' is `", GetClassName(cc), "`");
2088       auto nid = NameToInt(gn);
2089       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));
2090       known[nid] = cc;
2091       ++namedCount;
2092     }
2093     ++classCount;
2094   }
2095   writeln(classCount, " classes, ", namedCount, " game object classes.");
2099 // ////////////////////////////////////////////////////////////////////////// //
2100 #include "timelimit.vc"
2101 //const int TimeLimitDate = 2018232;
2104 void performTimeCheck () {
2105 #ifdef DISABLE_TIME_CHECK
2106 #else
2107   if (TigerEye) return;
2109   TTimeVal tv;
2110   if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2112   TDateTime tm;
2113   if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2115   int tldate = tm.year*1000+tm.yday;
2117   if (tldate > TimeLimitDate) {
2118     level.maxPlayingTime = 24;
2119   } else {
2120     //writeln("*** days left: ", TimeLimitDate-tldate);
2121   }
2122 #endif
2126 void setupCheats () {
2127   return;
2128   //startMode = StartMode.Dead;
2129   //startMode = StartMode.Title;
2130   //startMode = StartMode.Stars;
2131   //startMode = StartMode.Sun;
2132   startMode = StartMode.Moon;
2133   return;
2134   //global.scumGenSacrificePit = true;
2135   //global.scumAlwaysSacrificeAltar = true;
2137   // first lush jungle level
2138   //global.levelType = 1;
2139   /*
2140   global.scumGenCemetary = true;
2141   */
2142   //global.idol = false;
2143   //global.currLevel = 5;
2145   //global.isTunnelMan = true;
2146   //return;
2148   //global.currLevel = 5;
2149   //global.scumGenLake = true;
2151   //global.currLevel = 5;
2152   //global.currLevel = 9;
2153   //global.currLevel = 13;
2154   //global.currLevel = 14;
2155   //global.cheatEndGameSequence = 1;
2156   //return;
2158   //global.currLevel = 6;
2159   global.scumGenAlienCraft = true;
2160   global.currLevel = 9;
2161   //global.scumGenYetiLair = true;
2162   //global.genBlackMarket = true;
2163   //startDead = false;
2164   startMode = StartMode.Alive;
2165   return;
2167   global.cheatCanSkipOlmec = true;
2168   global.currLevel = 15;
2169   startMode = StartMode.Alive;
2170   return;
2172   global.scumGenShop = true;
2173   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2174   global.scumGenShopType = GameGlobal::ShopType.Craps;
2175   //global.scumGenShopType = 6; // craps
2176   //global.scumGenShopType = 7; // kissing
2178   //global.scumAlwaysSacrificeAltar = true;
2182 void setupSeeds () {
2186 // ////////////////////////////////////////////////////////////////////////// //
2187 void main () {
2188   checkGameObjNames();
2190   appSetName("k8spelunky");
2191   config = SpawnObject(GameConfig);
2192   global = SpawnObject(GameGlobal);
2193   global.config = config;
2194   config.heroType = GameConfig::Hero.Spelunker;
2196   global.randomizeSeedAll();
2198   fillCheatPickupList();
2199   fillCheatItemsList();
2200   fillCheatEnemiesList();
2202   loadGameOptions();
2203   loadKeyboardBindings();
2204   runGameLoop();