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