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