1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
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.
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/>
18 **********************************************************************************/
27 #ifndef DISABLE_TIME_CHECK
28 # define DISABLE_TIME_CHECK
34 //#define BIGGER_REPLAY_DATA
36 // ////////////////////////////////////////////////////////////////////////// //
37 #include "mapent/0all.vc"
38 #include "PlayerPawn.vc"
39 #include "PlayerPowerup.vc"
40 #include "GameLevel.vc"
43 // ////////////////////////////////////////////////////////////////////////// //
44 #include "uisimple.vc"
47 // ////////////////////////////////////////////////////////////////////////// //
48 class DebugSessionMovement : Object;
50 #ifdef BIGGER_REPLAY_DATA
51 array!(GameLevel::SavedKeyState) keypresses;
53 array!ubyte keypresses; // on each frame
55 GameConfig playconfig;
58 transient int otherSeed, roomSeed;
61 override void Destroy () {
63 keypresses.length = 0;
68 final void resetReplay () {
73 #ifndef BIGGER_REPLAY_DATA
74 final void addKey (int kbidx, bool down) {
75 if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
76 keypresses[$] = kbidx|(down ? 0x80 : 0);
80 final void addEndOfFrame () {
91 final int getKey (out int kbidx, out bool down) {
92 if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
93 if (keypos >= keypresses.length) return END_OF_RECORD;
94 ubyte b = keypresses[keypos++];
95 if (b == 0xff) return END_OF_FRAME;
103 // ////////////////////////////////////////////////////////////////////////// //
104 class TempOptionsKeys : Object;
106 int[16*GameConfig::MaxActionBinds] keybinds;
110 // ////////////////////////////////////////////////////////////////////////// //
113 transient string dbgSessionStateFileName = "debug_game_session_state";
114 transient string dbgSessionMovementFileName = "debug_game_session_movement";
115 const float dbgSessionSaveIntervalInSeconds = 30;
117 GLTexture texTigerEye;
121 SpriteStore sprStore;
122 BackTileStore bgtileStore;
127 int mouseX = int.min, mouseY = int.min;
128 int mouseLevelX = int.min, mouseLevelY = int.min;
129 bool renderMouseTile;
130 bool renderMouseRect;
142 StartMode startMode = StartMode.Intro;
146 bool replayFastForward = false;
147 int replayFastForwardSpeed = 2;
148 bool saveGameSession = false;
149 bool replayGameSession = false;
155 Replay doGameSavingPlaying = Replay.None;
156 float saveMovementLastTime = 0;
157 DebugSessionMovement debugMovement;
158 GameStats origStats; // for replaying
159 GameConfig origConfig; // for replaying
160 GameGlobal::SavedSeeds origSeeds;
165 transient bool allowRender = true;
169 transient int maskSX, maskSY;
170 transient SpriteImage smask;
171 transient int maskFrame;
175 // ////////////////////////////////////////////////////////////////////////// //
176 final void saveKeyboardBindings () {
177 auto tok = SpawnObject(TempOptionsKeys);
178 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
179 appSaveOptions(tok, "keybindings");
184 final void loadKeyboardBindings () {
185 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
187 if (tok.kbversion != TempOptionsKeys.default.kbversion) {
188 global.config.resetKeybindings();
190 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
197 // ////////////////////////////////////////////////////////////////////////// //
198 void saveGameOptions () {
199 appSaveOptions(global.config, "config");
203 void loadGameOptions () {
204 auto cfg = appLoadOptions(GameConfig, "config");
206 auto oldHero = config.heroType;
207 auto tok = SpawnObject(TempOptionsKeys);
208 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
209 delete global.config;
212 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
214 writeln("config loaded");
215 global.restartMusic();
217 //config.heroType = GameConfig::Hero.Spelunker;
218 config.heroType = oldHero;
221 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
225 // ////////////////////////////////////////////////////////////////////////// //
226 void saveGameStats () {
227 if (level.stats) appSaveOptions(level.stats, "stats");
231 void loadGameStats () {
232 auto stats = appLoadOptions(GameStats, "stats");
237 if (!level.stats) level.stats = SpawnObject(GameStats);
238 level.stats.global = global;
242 // ////////////////////////////////////////////////////////////////////////// //
243 struct UIPaneSaveInfo {
245 UIPane::SaveInfo nfo;
248 transient UIPane optionsPane; // either options, or binding editor
250 transient GameLevel::IVec2D optionsPaneOfs;
251 transient void delegate () saveOptionsDG;
253 transient array!UIPaneSaveInfo optionsPaneState;
256 final void saveCurrentPane () {
257 if (!optionsPane || !optionsPane.id) return;
260 if (optionsPane.id == 'CheatFlags') {
261 if (instantGhost && level.ghostTimeLeft > 0) {
262 level.ghostTimeLeft = 1;
266 foreach (ref auto psv; optionsPaneState) {
267 if (psv.id == optionsPane.id) {
268 optionsPane.saveState(psv.nfo);
273 optionsPaneState.length += 1;
274 optionsPaneState[$-1].id = optionsPane.id;
275 optionsPane.saveState(optionsPaneState[$-1].nfo);
279 final void restoreCurrentPane () {
280 if (optionsPane) optionsPane.setupHotkeys(); // why not?
281 if (!optionsPane || !optionsPane.id) return;
282 foreach (ref auto psv; optionsPaneState) {
283 if (psv.id == optionsPane.id) {
284 optionsPane.restoreState(psv.nfo);
291 // ////////////////////////////////////////////////////////////////////////// //
292 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
293 if (!it.tagClass) return;
294 if (class!MapObject(it.tagClass)) {
295 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
296 it.owner.closeMe = true;
301 // ////////////////////////////////////////////////////////////////////////// //
302 transient array!(class!MapObject) cheatItemsList;
305 final void fillCheatItemsList () {
306 cheatItemsList.length = 0;
307 cheatItemsList[$] = ItemProjectileArrow;
308 cheatItemsList[$] = ItemWeaponShotgun;
309 cheatItemsList[$] = ItemWeaponAshShotgun;
310 cheatItemsList[$] = ItemWeaponPistol;
311 cheatItemsList[$] = ItemWeaponMattock;
312 cheatItemsList[$] = ItemWeaponMachete;
313 cheatItemsList[$] = ItemWeaponWebCannon;
314 cheatItemsList[$] = ItemWeaponSceptre;
315 cheatItemsList[$] = ItemWeaponBow;
316 cheatItemsList[$] = ItemBones;
317 cheatItemsList[$] = ItemFakeBones;
318 cheatItemsList[$] = ItemFishBone;
319 cheatItemsList[$] = ItemRock;
320 cheatItemsList[$] = ItemJar;
321 cheatItemsList[$] = ItemSkull;
322 cheatItemsList[$] = ItemGoldenKey;
323 cheatItemsList[$] = ItemGoldIdol;
324 cheatItemsList[$] = ItemCrystalSkull;
325 cheatItemsList[$] = ItemShellSingle;
326 cheatItemsList[$] = ItemChest;
327 cheatItemsList[$] = ItemCrate;
328 cheatItemsList[$] = ItemLockedChest;
329 cheatItemsList[$] = ItemDice;
330 cheatItemsList[$] = ItemBasketBall;
334 final UIPane createCheatItemsPane () {
335 if (!level.player) return none;
337 UIPane pane = SpawnObject(UIPane);
339 pane.sprStore = sprStore;
341 pane.width = 320*3-64;
342 pane.height = 240*3-64;
344 foreach (auto ipk; cheatItemsList) {
345 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
349 //optionsPaneOfs.x = 100;
350 //optionsPaneOfs.y = 50;
356 // ////////////////////////////////////////////////////////////////////////// //
357 transient array!(class!MapObject) cheatEnemiesList;
360 final void fillCheatEnemiesList () {
361 cheatEnemiesList.length = 0;
362 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
363 cheatEnemiesList[$] = EnemyBat;
364 cheatEnemiesList[$] = EnemySpiderHang;
365 cheatEnemiesList[$] = EnemySpider;
366 cheatEnemiesList[$] = EnemySnake;
367 cheatEnemiesList[$] = EnemyCaveman;
368 cheatEnemiesList[$] = EnemySkeleton;
369 cheatEnemiesList[$] = MonsterShopkeeper;
370 cheatEnemiesList[$] = EnemyZombie;
371 cheatEnemiesList[$] = EnemyVampire;
372 cheatEnemiesList[$] = EnemyFrog;
373 cheatEnemiesList[$] = EnemyGreenFrog;
374 cheatEnemiesList[$] = EnemyFireFrog;
375 cheatEnemiesList[$] = EnemyMantrap;
376 cheatEnemiesList[$] = EnemyScarab;
377 cheatEnemiesList[$] = EnemyFloater;
378 cheatEnemiesList[$] = EnemyBlob;
379 cheatEnemiesList[$] = EnemyMonkey;
380 cheatEnemiesList[$] = EnemyGoldMonkey;
381 cheatEnemiesList[$] = EnemyAlien;
382 cheatEnemiesList[$] = EnemyYeti;
383 cheatEnemiesList[$] = EnemyHawkman;
384 cheatEnemiesList[$] = EnemyUFO;
385 cheatEnemiesList[$] = EnemyYetiKing;
389 final UIPane createCheatEnemiesPane () {
390 if (!level.player) return none;
392 UIPane pane = SpawnObject(UIPane);
394 pane.sprStore = sprStore;
396 pane.width = 320*3-64;
397 pane.height = 240*3-64;
399 foreach (auto ipk; cheatEnemiesList) {
400 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
404 //optionsPaneOfs.x = 100;
405 //optionsPaneOfs.y = 50;
411 // ////////////////////////////////////////////////////////////////////////// //
412 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
415 final void fillCheatPickupList () {
416 cheatPickupList.length = 0;
417 cheatPickupList[$] = ItemPickupBombBag;
418 cheatPickupList[$] = ItemPickupBombBox;
419 cheatPickupList[$] = ItemPickupPaste;
420 cheatPickupList[$] = ItemPickupRopePile;
421 cheatPickupList[$] = ItemPickupShellBox;
422 cheatPickupList[$] = ItemPickupAnkh;
423 cheatPickupList[$] = ItemPickupCape;
424 cheatPickupList[$] = ItemPickupJetpack;
425 cheatPickupList[$] = ItemPickupUdjatEye;
426 cheatPickupList[$] = ItemPickupCrown;
427 cheatPickupList[$] = ItemPickupKapala;
428 cheatPickupList[$] = ItemPickupParachute;
429 cheatPickupList[$] = ItemPickupCompass;
430 cheatPickupList[$] = ItemPickupSpectacles;
431 cheatPickupList[$] = ItemPickupGloves;
432 cheatPickupList[$] = ItemPickupMitt;
433 cheatPickupList[$] = ItemPickupJordans;
434 cheatPickupList[$] = ItemPickupSpringShoes;
435 cheatPickupList[$] = ItemPickupSpikeShoes;
436 cheatPickupList[$] = ItemPickupTeleporter;
440 final UIPane createCheatPickupsPane () {
441 if (!level.player) return none;
443 UIPane pane = SpawnObject(UIPane);
445 pane.sprStore = sprStore;
447 pane.width = 320*3-64;
448 pane.height = 240*3-64;
450 foreach (auto ipk; cheatPickupList) {
451 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
455 //optionsPaneOfs.x = 100;
456 //optionsPaneOfs.y = 50;
462 // ////////////////////////////////////////////////////////////////////////// //
463 transient int instantGhost;
465 final UIPane createCheatFlagsPane () {
466 UIPane pane = SpawnObject(UIPane);
467 pane.id = 'CheatFlags';
468 pane.sprStore = sprStore;
470 pane.width = 320*3-64;
471 pane.height = 240*3-64;
475 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
476 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
477 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
478 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
479 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
480 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
481 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
482 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
483 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
484 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
485 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
486 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
487 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
488 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
489 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
490 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
491 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
492 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
494 optionsPaneOfs.x = 100;
495 optionsPaneOfs.y = 50;
501 final UIPane createOptionsPane () {
502 UIPane pane = SpawnObject(UIPane);
504 pane.sprStore = sprStore;
506 pane.width = 320*3-64;
507 pane.height = 240*3-64;
511 //!UICheckBox.Create(pane, &config.useFrozenRegion, "FROZEN REGION", "OFF-SCREEN ENTITIES ARE PAUSED TO IMPROVE PERFORMANCE. LEAVE THIS ENABLED IF YOU DON'T KNOW WHAT IT IS. DO A WEB SEARCH FOR 'SPELUNKY FROZEN REGION' FOR A FULL EXPLANATION. THE YASM README FILE ALSO HAS INFO.");
514 UILabel.Create(pane, "VISUAL OPTIONS");
515 UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
516 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
517 UICheckBox.Create(pane, &config.alwaysCenterPlayer, "ALWAYS KEEP PLAYER IN CENTER", "ALWAYS KEEP PLAYER IN THE CENTER OF THE SCREEN. IF THIS OPTION IS UNSET, PLAYER WILL BE ALLOWED TO MOVE SLIGHTLY BEFORE THE VIEWPORT STARTS FOLLOWING HIM (THIS IS HOW IT WAS DONE IN THE ORIGINAL GAME).");
518 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
519 auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
520 startfs.onValueChanged = delegate void (int newval) {
525 auto fsmode = UIIntEnum.Create(pane, &config.fsmode, 1, 2, "FULLSCREEN MODE: ", "YOU CAN CHOOSE EITHER REAL FULLSCREEN MODE, OR SCALED. USUALLY, SCALED WORKS BETTER.");
526 fsmode.names[$] = "REAL";
527 fsmode.names[$] = "SCALED";
528 fsmode.onValueChanged = delegate void (int newval) {
534 auto fsres = UIIntEnum.Create(pane, &config.realfsres, 0, GameConfig::RealFSModes.MAX, "FULLSCREEN RESOLUTION: ", "SELECT RESOLUTION FOR REAL FULLSCREEN MODE.");
535 fsres.names[$] = "1024x768";
536 fsres.names[$] = "1280x960";
537 fsres.names[$] = "1280x1024";
538 fsres.names[$] = "1600x1200";
539 fsres.names[$] = "1680x1050";
540 fsres.names[$] = "1920x1080";
541 fsres.names[$] = "1920x1200";
544 UILabel.Create(pane, "");
545 UILabel.Create(pane, "HUD OPTIONS");
546 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
547 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.");
548 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
551 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
555 UILabel.Create(pane, "");
556 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
557 //!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.");
558 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
559 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
560 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.");
561 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.");
562 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
563 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.");
564 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
567 UILabel.Create(pane, "");
568 UILabel.Create(pane, "GAMEPLAY OPTIONS");
569 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.");
570 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
571 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
572 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!");
573 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.");
574 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.");
575 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
576 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.");
577 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.");
578 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.");
579 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.");
580 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?");
581 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.");
582 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.");
583 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.");
584 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
585 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
586 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
587 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
588 rstl.names[$] = "RANDOM";
589 rstl.names[$] = "NORMAL";
590 rstl.names[$] = "BIZARRE";
593 UILabel.Create(pane, "");
594 UILabel.Create(pane, "WHIP OPTIONS");
595 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
596 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
597 whiptype.names[$] = "NORMAL";
598 whiptype.names[$] = "LONG";
599 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.");
602 UILabel.Create(pane, "");
603 UILabel.Create(pane, "PLAYER OPTIONS");
604 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
605 herotype.names[$] = "SPELUNKY GUY";
606 herotype.names[$] = "DAMSEL";
607 herotype.names[$] = "TUNNEL MAN";
610 UILabel.Create(pane, "");
611 UILabel.Create(pane, "CHEAT OPTIONS");
612 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.");
613 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
614 plrlit.names[$] = "NEVER";
615 plrlit.names[$] = "FORCED DARKNESS";
616 plrlit.names[$] = "ALWAYS";
617 UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
618 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'.");
619 rdark.names[$] = "NEVER";
620 rdark.names[$] = "DEFAULT";
621 rdark.names[$] = "ALWAYS";
622 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.");
624 rghost.getNameCB = delegate string (int val) {
625 if (val < 0) return "INSTANT";
626 if (val == 0) return "NEVER";
627 if (val < 120) return va("%d SEC", val);
628 if (val%60 == 0) return va("%d MIN", val/60);
629 if (val%60 == 30) return va("%d.5 MIN", val/60);
630 return va("%d MIN, %d SEC", val/60, val%60);
632 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.");
634 UILabel.Create(pane, "");
635 UILabel.Create(pane, "CHEAT START OPTIONS");
636 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.");
637 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
638 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
639 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
640 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
643 UILabel.Create(pane, "");
644 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
645 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
646 mm.names[$] = "SILENCE";
647 mm.names[$] = "RESTART";
648 mm.names[$] = "DON'T TOUCH";
650 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
651 //mm.names[$] = "SILENCE";
652 mm.names[$] = "RESTART";
653 mm.names[$] = "DON'T TOUCH";
656 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
658 swstereo.onValueChanged = delegate void (int newval) {
659 SoundSystem.SwapStereo = newval;
663 UILabel.Create(pane, "");
664 UILabel.Create(pane, "SOUND CONTROL CENTER");
665 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
666 rmusonoff.onValueChanged = delegate void (int newval) {
667 global.restartMusic();
670 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
672 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
673 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
675 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
676 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
679 saveOptionsDG = delegate void () {
680 writeln("saving options");
683 optionsPaneOfs.x = 42;
684 optionsPaneOfs.y = 0;
690 final void createBindingsControl (UIPane pane, int keyidx) {
693 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
694 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
695 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
696 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
697 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
698 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
699 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
700 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
701 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
702 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
703 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
706 int arridx = GameConfig.getKeyIndex(keyidx);
707 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
711 final UIPane createBindingsPane () {
712 UIPane pane = SpawnObject(UIPane);
713 pane.id = 'KeyBindings';
714 pane.sprStore = sprStore;
716 pane.width = 320*3-64;
717 pane.height = 240*3-64;
719 createBindingsControl(pane, GameConfig::Key.Left);
720 createBindingsControl(pane, GameConfig::Key.Right);
721 createBindingsControl(pane, GameConfig::Key.Up);
722 createBindingsControl(pane, GameConfig::Key.Down);
723 createBindingsControl(pane, GameConfig::Key.Jump);
724 createBindingsControl(pane, GameConfig::Key.Run);
725 createBindingsControl(pane, GameConfig::Key.Attack);
726 createBindingsControl(pane, GameConfig::Key.Switch);
727 createBindingsControl(pane, GameConfig::Key.Pay);
728 createBindingsControl(pane, GameConfig::Key.Bomb);
729 createBindingsControl(pane, GameConfig::Key.Rope);
731 saveOptionsDG = delegate void () {
732 writeln("saving keys");
733 saveKeyboardBindings();
735 optionsPaneOfs.x = 120;
736 optionsPaneOfs.y = 140;
742 // ////////////////////////////////////////////////////////////////////////// //
743 void clearGameMovement () {
744 debugMovement = SpawnObject(DebugSessionMovement);
745 debugMovement.playconfig = SpawnObject(GameConfig);
746 debugMovement.playconfig.copyGameplayConfigFrom(config);
747 debugMovement.resetReplay();
751 void saveGameMovement (string fname, optional bool packit) {
752 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
753 saveMovementLastTime = GetTickCount();
757 void loadGameMovement (string fname) {
758 delete debugMovement;
759 debugMovement = appLoadOptions(DebugSessionMovement, fname);
760 debugMovement.resetReplay();
763 origStats = level.stats;
764 origStats.global = none;
765 level.stats = SpawnObject(GameStats);
766 level.stats.global = global;
769 config = debugMovement.playconfig;
770 global.config = config;
771 global.saveSeeds(origSeeds);
776 void stopReplaying () {
778 global.restoreSeeds(origSeeds);
780 delete debugMovement;
781 saveGameSession = false;
782 replayGameSession = false;
783 doGameSavingPlaying = Replay.None;
786 origStats.global = global;
787 level.stats = origStats;
793 global.config = origConfig;
799 // ////////////////////////////////////////////////////////////////////////// //
800 final bool saveGame (string gmname) {
801 return appSaveOptions(level, gmname);
805 final bool loadGame (string gmname) {
806 auto olddel = GC_ImmediateDelete;
807 GC_ImmediateDelete = false;
809 auto stats = level.stats;
812 auto lvl = appLoadOptions(GameLevel, gmname);
814 //lvl.global.config = config;
819 level.loserGPU = loserGPU;
820 global = level.global;
821 global.config = config;
823 level.sprStore = sprStore;
824 level.bgtileStore = bgtileStore;
827 level.onBeforeFrame = &beforeNewFrame;
828 level.onAfterFrame = &afterNewFrame;
829 level.onInterFrame = &interFrame;
830 level.onLevelExitedCB = &levelExited;
831 level.onCameraTeleported = &cameraTeleportedCB;
833 //level.viewWidth = GLVideo.screenWidth;
834 //level.viewHeight = GLVideo.screenHeight;
835 level.viewWidth = 320*3;
836 level.viewHeight = 240*3;
839 level.centerViewAtPlayer();
840 teleportCameraAt(level.viewStart);
842 recalcCameraCoords(0);
847 level.stats.global = level.global;
849 GC_ImmediateDelete = olddel;
850 GC_CollectGarbage(true); // destroy delayed objects too
855 // ////////////////////////////////////////////////////////////////////////// //
856 float lastThinkerTime;
857 int replaySkipFrame = 0;
860 final void onTimePasses () {
861 float curTime = GetTickCount();
862 if (lastThinkerTime > 0) {
863 if (curTime < lastThinkerTime) {
864 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
865 lastThinkerTime = curTime;
868 if (replayFastForward && replaySkipFrame) {
870 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
873 level.processThinkers(curTime-lastThinkerTime);
875 lastThinkerTime = curTime;
879 final void resetFramesAndForceOne () {
880 float curTime = GetTickCount();
881 lastThinkerTime = curTime;
883 auto wasPaused = level.gamePaused;
884 level.gamePaused = false;
885 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
886 level.processThinkers(GameLevel::FrameTime);
887 level.gamePaused = wasPaused;
888 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
892 // ////////////////////////////////////////////////////////////////////////// //
893 private float currFrameDelta; // so level renderer can properly interpolate the player
894 private GameLevel::IVec2D camPrev, camCurr;
895 private GameLevel::IVec2D camShake;
896 private GameLevel::IVec2D viewCameraPos;
899 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
904 viewCameraPos.x = pos.x;
905 viewCameraPos.y = pos.y;
911 // call `recalcCameraCoords()` to get real camera coords after this
912 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
913 // check if camera is moved too far, and teleport it
915 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
916 abs(camCurr.y-pos.y)/global.scale >= 16*4))
918 teleportCameraAt(pos);
920 camPrev.x = camCurr.x;
921 camPrev.y = camCurr.y;
925 camShake.x = level.shakeDir.x*global.scale;
926 camShake.y = level.shakeDir.y*global.scale;
930 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
931 currFrameDelta = frameDelta;
932 viewCameraPos.x = roundi(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
933 viewCameraPos.y = roundi(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
935 viewCameraPos.x += camShake.x;
936 viewCameraPos.y += camShake.y;
940 GameLevel::SavedKeyState savedKeyState;
942 final void pauseGame () {
943 if (!level.gamePaused) {
944 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
945 level.gamePaused = true;
946 global.pauseAllSounds();
951 final void unpauseGame () {
952 if (level.gamePaused) {
953 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
954 level.gamePaused = false;
955 level.gameShowHelp = false;
956 level.gameHelpScreen = 0;
957 //lastThinkerTime = 0;
958 global.resumeAllSounds();
960 pauseRequested = false;
961 helpRequested = false;
966 final void beforeNewFrame (bool frameSkip) {
969 level.disablePlayerThink = true;
972 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
973 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
974 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
976 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
977 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
978 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
979 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
981 level.disablePlayerThink = false;
987 if (!level.gamePaused) {
988 // save seeds for afterframe processing
990 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
991 debugMovement.otherSeed = global.globalOtherSeed;
992 debugMovement.roomSeed = global.globalRoomSeed;
996 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
998 #ifdef BIGGER_REPLAY_DATA
999 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
1000 debugMovement.keypresses.length += 1;
1001 level.keysSaveState(debugMovement.keypresses[$-1]);
1002 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
1003 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
1007 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
1008 #ifdef BIGGER_REPLAY_DATA
1009 if (debugMovement.keypos < debugMovement.keypresses.length) {
1010 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1011 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1012 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1013 ++debugMovement.keypos;
1019 auto code = debugMovement.getKey(out kbidx, out down);
1020 if (code == DebugSessionMovement::END_OF_RECORD) {
1021 // do this in main loop, so we can view totals
1025 if (code == DebugSessionMovement::END_OF_FRAME) {
1028 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1029 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1037 final void afterNewFrame (bool frameSkip) {
1038 if (!replayFastForward) replaySkipFrame = 0;
1040 if (level.gamePaused) return;
1042 if (!level.gamePaused) {
1043 if (doGameSavingPlaying != Replay.None) {
1044 if (doGameSavingPlaying == Replay.Saving) {
1045 replayFastForward = false; // just in case
1046 #ifndef BIGGER_REPLAY_DATA
1047 debugMovement.addEndOfFrame();
1049 auto stt = GetTickCount();
1050 if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1051 } else if (doGameSavingPlaying == Replay.Replaying) {
1052 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1053 replaySkipFrame = 1;
1059 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1060 //SoundSystem.UpdateSounds();
1062 //if (!freeRide) level.fixCamera();
1063 setNewCameraPos(level.viewStart);
1065 prevCameraX = currCameraX;
1066 prevCameraY = currCameraY;
1067 currCameraX = level.cameraX;
1068 currCameraY = level.cameraY;
1069 // disable camera interpolation if the screen is shaking
1070 if (level.shakeX|level.shakeY) {
1071 prevCameraX = currCameraX;
1072 prevCameraY = currCameraY;
1075 // disable camera interpolation if it moves too far away
1076 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1077 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1079 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1081 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1082 pauseRequested = false;
1084 if (helpRequested) {
1085 helpRequested = false;
1086 level.gameShowHelp = true;
1087 level.gameHelpScreen = 0;
1090 if (!showHelp) showHelp = true;
1092 writeln("active objects in level: ", level.activeItemsCount);
1098 final void interFrame (float frameDelta) {
1099 if (!config.interpolateMovement) return;
1100 recalcCameraCoords(frameDelta);
1104 final void cameraTeleportedCB () {
1105 teleportCameraAt(level.viewStart);
1106 recalcCameraCoords(0);
1110 // ////////////////////////////////////////////////////////////////////////// //
1112 final void setColorByIdx (bool isset, int col) {
1114 // missed collision: red
1115 GLVideo.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1116 } else if (col == -999) {
1117 // superfluous collision: blue
1118 GLVideo.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1119 } else if (col <= 0) {
1120 // no collision: yellow
1121 GLVideo.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1122 } else if (col > 0) {
1124 GLVideo.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1129 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1131 CollisionMask cm = CollisionMask.Create(frm, false);
1133 int scale = global.config.scale;
1134 int bx0, by0, bx1, by1;
1135 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1136 GLVideo.color = 0x7f_00_00_ff;
1137 GLVideo.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1138 if (!cm.isEmptyMask) {
1139 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1140 foreach (int iy; 0..cm.height) {
1141 foreach (int ix; 0..cm.width) {
1142 int v = cm.mask[ix, iy];
1143 foreach (int dx; 0..32) {
1146 GLVideo.color = 0x3f_00_ff_00;
1147 GLVideo.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1156 foreach (int iy; 0..frm.tex.height) {
1157 foreach (int ix; 0..(frm.tex.width+31)/31) {
1158 foreach (int dx; 0..32) {
1160 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1161 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1162 setColorByIdx(true, col);
1163 if (col <= 0) GLVideo.color = 0xaf_ff_ff_00;
1165 GLVideo.color = 0xaf_00_ff_00;
1167 GLVideo.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1173 if (frm.bw > 0 && frm.bh > 0) {
1174 setColorByIdx(true, col);
1175 GLVideo.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1176 GLVideo.color = 0xff_00_00;
1177 GLVideo.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1186 // ////////////////////////////////////////////////////////////////////////// //
1187 transient int drawStats;
1188 transient array!int statsTopItem;
1191 final int totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1192 auto sa = string(a.objName).toUpperCase;
1193 auto sb = string(b.objName).toUpperCase;
1194 if (sa < sb) return -1;
1195 if (sa > sb) return 1;
1200 final int getStatsTopItem () {
1201 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1205 final void setStatsTopItem (int val) {
1206 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1207 statsTopItem[drawStats] = val;
1211 final void resetStatsTopItem () {
1216 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1217 sprStore.loadFont('sFontSmall');
1223 final int calcStatsVisItems () {
1226 statsDrawGetStartPosLoadFont(currX, currY);
1227 int endY = level.viewHeight-(currY*2);
1228 return max(1, endY/sprStore.getFontHeight(scale));
1232 int getStatsItemCount () {
1233 switch (drawStats) {
1234 case 2: return level.stats.totalKills.length;
1235 case 3: return level.stats.totalDeaths.length;
1236 case 4: return level.stats.totalCollected.length;
1242 final void statsMoveUp () {
1243 int count = getStatsItemCount();
1244 if (count < 0) return;
1245 int visItems = calcStatsVisItems();
1246 if (count <= visItems) { resetStatsTopItem(); return; }
1247 int top = getStatsTopItem();
1249 setStatsTopItem(top-1);
1253 final void statsMoveDown () {
1254 int count = getStatsItemCount();
1255 if (count < 0) return;
1256 int visItems = calcStatsVisItems();
1257 if (count <= visItems) { resetStatsTopItem(); return; }
1258 int top = getStatsTopItem();
1259 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1260 top = clamp(top+1, 0, count-visItems);
1261 setStatsTopItem(top);
1265 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1266 arr.sort(&totalsNameCmpCB);
1270 statsDrawGetStartPosLoadFont(currX, currY);
1272 int endY = level.viewHeight-(currY*2);
1273 int visItems = calcStatsVisItems();
1275 if (arr.length <= visItems) resetStatsTopItem();
1277 int topItem = getStatsTopItem();
1281 GLVideo.color = 0x3f_ff_ff_00;
1282 auto spr = sprStore['sPageUp'];
1283 spr.frames[0].blitAt(currX-28, currY, scale);
1286 // "downscroll" mark
1287 if (topItem+visItems < arr.length) {
1288 GLVideo.color = 0x3f_ff_ff_00;
1289 auto spr = sprStore['sPageDown'];
1290 spr.frames[0].blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1293 GLVideo.color = 0xff_ff_00;
1294 int hiColor = 0x00_ff_00;
1295 int hiColor1 = 0xf_ff_ff;
1298 while (it < arr.length && visItems-- > 0) {
1299 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);
1300 currY += sprStore.getFontHeight(scale);
1306 void drawStatsScreen () {
1307 int deathCount, killCount, collectCount;
1309 sprStore.loadFont('sFontSmall');
1311 GLVideo.color = 0xff_ff_ff;
1312 level.drawTextAtS3Centered(240-2-8, "ESC-RETURN F10-QUIT CTRL+DEL-SUICIDE");
1313 level.drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
1315 GLVideo.color = 0xff_ff_00;
1316 int hiColor = 0x00_ff_00;
1318 switch (drawStats) {
1319 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1320 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1321 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1324 if (drawStats > 1) {
1326 foreach (ref auto i; statsTopItem) i = 0;
1331 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1332 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1333 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1339 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1340 currY += sprStore.getFontHeight(scale);
1342 int gw = level.stats.gamesWon;
1343 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1344 currY += sprStore.getFontHeight(scale);
1346 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1347 currY += sprStore.getFontHeight(scale);
1349 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1350 currY += sprStore.getFontHeight(scale);
1352 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1353 currY += sprStore.getFontHeight(scale);
1355 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1356 currY += sprStore.getFontHeight(scale);
1358 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1359 currY += sprStore.getFontHeight(scale);
1361 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1362 currY += sprStore.getFontHeight(scale);
1364 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1365 currY += sprStore.getFontHeight(scale);
1367 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1368 currY += sprStore.getFontHeight(scale);
1370 int gs = level.stats.totalGhostSummoned;
1371 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1372 currY += sprStore.getFontHeight(scale);
1374 currY += sprStore.getFontHeight(scale);
1375 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1376 currY += sprStore.getFontHeight(scale);
1380 bool frameRendered = true;
1383 if (level) level.keysNextFrame();
1384 frameRendered = false;
1389 frameRendered = true;
1391 if (GLVideo.frameTime == 0) {
1393 GLVideo.requestRefresh();
1398 if (level.framesProcessedFromLastClear < 1) return;
1399 calcMouseMapCoords();
1401 GLVideo.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1402 GLVideo.clearScreen();
1403 GLVideo.stencil = false;
1404 GLVideo.color = 0xff_ff_ff;
1405 GLVideo.textureFiltering = false;
1406 // don't touch framebuffer alpha
1407 GLVideo.colorMask = GLVideo::CMask.Colors;
1409 GLVideo::ScissorRect scsave;
1410 bool doRestoreGL = false;
1413 if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1415 GLVideo.getScissor(scsave);
1416 GLVideo.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1417 GLVideo.glPushMatrix();
1418 GLVideo.glTranslate(level.viewOffsetX, level.viewOffsetY);
1422 if (level.viewWidth != GLVideo.screenWidth || level.viewHeight != GLVideo.screenHeight) {
1424 float scx = float(GLVideo.screenWidth)/float(level.viewWidth);
1425 float scy = float(GLVideo.screenHeight)/float(level.viewHeight);
1426 float scale = fmin(scx, scy);
1427 int calcedW = trunci(level.viewWidth*scale);
1428 int calcedH = trunci(level.viewHeight*scale);
1429 GLVideo.getScissor(scsave);
1430 int ofsx = (GLVideo.screenWidth-calcedW)/2;
1431 int ofsy = (GLVideo.screenHeight-calcedH)/2;
1432 GLVideo.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1433 GLVideo.glPushMatrix();
1434 GLVideo.glTranslate(ofsx, ofsy);
1435 GLVideo.glScale(scale, scale);
1438 //level.viewOffsetX = (GLVideo.screenWidth-320*3)/2;
1439 //level.viewOffsetY = (GLVideo.screenHeight-240*3)/2;
1443 level.viewOffsetX = 0;
1444 level.viewOffsetY = 0;
1445 GLVideo.glScale(float(GLVideo.screenWidth)/float(level.viewWidth), float(GLVideo.screenHeight)/float(level.viewHeight));
1448 float scx = float(GLVideo.screenWidth)/float(level.viewWidth);
1449 float scy = float(GLVideo.screenHeight)/float(level.viewHeight);
1450 GLVideo.glScale(float(GLVideo.screenWidth)/float(level.viewWidth), 1);
1456 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1459 if (level.gamePaused && showHelp != 2) {
1460 if (mouseLevelX != int.min) {
1461 int scale = level.global.scale;
1462 if (renderMouseRect) {
1463 GLVideo.color = 0xcf_ff_ff_00;
1464 GLVideo.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1466 if (renderMouseTile) {
1467 GLVideo.color = 0xaf_ff_00_00;
1468 GLVideo.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1473 switch (doGameSavingPlaying) {
1475 GLVideo.color = 0x7f_00_ff_00;
1476 sprStore.loadFont('sFont');
1477 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1479 case Replay.Replaying:
1480 if (level.player && !level.player.dead) {
1481 GLVideo.color = 0x7f_ff_00_00;
1482 sprStore.loadFont('sFont');
1483 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1484 int th = sprStore.getFontHeight(2);
1485 if (replayFastForward) {
1486 sprStore.loadFont('sFontSmall');
1487 string sstr = va("x%d", replayFastForwardSpeed+1);
1488 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1493 if (saveGameSession) {
1494 GLVideo.color = 0x7f_ff_7f_00;
1495 sprStore.loadFont('sFont');
1496 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1502 if (level.player && level.player.dead && !showHelp) {
1504 GLVideo.color = 0x8f_00_00_00;
1505 GLVideo.fillRect(0, 0, level.viewWidth, level.viewHeight);
1510 if (true /*level.inWinCutscene == 0*/) {
1511 GLVideo.color = 0xff_ff_ff;
1512 sprStore.loadFont('sFontSmall');
1513 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1515 "PRESS $PAY TO RESTART GAME\n"~
1517 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1519 "TOTAL PLAYING TIME: |%s|"~
1521 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1522 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1523 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1525 GameLevel.time2str(level.stats.playingTime)
1527 kmsg = global.expandString(kmsg);
1528 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1535 GLVideo.color = 0xff_7f_00;
1536 sprStore.loadFont('sFontSmall');
1537 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1538 auto spf = smask.frames[maskFrame];
1539 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1541 spf.bx, spf.by, spf.bw, spf.bh,
1542 (spf.maskEmpty ? "TAN" : "ONA"),
1543 (spf.precise ? "TAN" : "ONA")),
1546 //spf.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1547 //writeln("pos=(", maskSX, ",", maskSY, ")");
1548 int scale = global.config.scale;
1549 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1550 int mapX = xofs/scale+maskSX;
1551 int mapY = yofs/scale+maskSY;
1554 writeln("==== tiles ====");
1556 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1557 if (t.spectral || !t.isInstanceAlive) return false;
1558 GLVideo.color = 0x7f_ff_00_00;
1559 GLVideo.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);
1560 auto tsf = t.getSpriteFrame();
1562 auto spf = smask.frames[maskFrame];
1563 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1564 int mapX = xofs/global.config.scale+maskSX;
1565 int mapY = yofs/global.config.scale+maskSY;
1568 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1569 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1570 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1574 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1575 GLVideo.color = 0x7f_ff_00_00;
1576 GLVideo.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);
1580 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1582 GLVideo.color = 0xaf_ff_ff_ff;
1583 spf.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1584 GLVideo.color = 0xff_ff_00;
1585 GLVideo.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1589 int fx0, fy0, fx1, fy1;
1590 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1591 GLVideo.color = 0x7f_00_00_ff;
1592 GLVideo.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1598 GLVideo.color = 0x8f_00_00_00;
1599 GLVideo.fillRect(0, 0, level.viewWidth, level.viewHeight);
1601 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1606 GLVideo.color = 0xff_ff_00;
1607 //if (showHelp > 1) GLVideo.color = 0xaf_ff_ff_00;
1608 if (showHelp == 1) {
1609 int msx, msy, ww, wh;
1610 GLVideo.getMousePos(out msx, out msy);
1611 GLVideo.getRealWindowSize(out ww, out wh);
1612 if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1613 sprStore.loadFont('sFontSmall');
1614 GLVideo.color = 0xff_ff_00;
1615 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1616 "F1: show this help\n"~
1618 "K : redefine keys\n"~
1619 "I : toggle interpolaion\n"~
1620 "N : create some blood\n"~
1621 "R : generate a new level\n"~
1622 "F : toggle \"Frozen Area\"\n"~
1623 "X : resurrect player\n"~
1624 "Q : teleport to exit\n"~
1625 "D : teleport to damel\n"~
1627 "C : cheat flags menu\n"~
1628 "P : cheat pickup menu\n"~
1629 "E : cheat enemy menu\n"~
1630 "Enter: cheat items menu\n"~
1632 "TAB: toggle 'freeroam' mode\n"~
1637 if (level) level.renderPauseOverlay();
1641 //SoundSystem.UpdateSounds();
1643 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1646 GLVideo.setScissor(scsave);
1647 GLVideo.glPopMatrix();
1652 GLVideo.color = 0xaf_ff_ff_ff;
1653 texTigerEye.blitAt(GLVideo.screenWidth-texTigerEye.width-2, GLVideo.screenHeight-texTigerEye.height-2);
1658 // ////////////////////////////////////////////////////////////////////////// //
1659 transient bool gameJustOver;
1660 transient bool waitingForPayRestart;
1663 final void calcMouseMapCoords () {
1664 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1665 mouseLevelX = int.min;
1666 mouseLevelY = int.min;
1669 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1670 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1671 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1675 final void onEvent (ref event_t evt) {
1676 if (evt.type == ev_closequery) { GLVideo.requestQuit(); return; }
1678 if (evt.type == ev_winfocus) {
1679 if (level && !evt.focused) {
1683 //writeln("FOCUS!");
1684 GLVideo.getMousePos(out mouseX, out mouseY);
1689 if (evt.type == ev_uimouse) {
1692 calcMouseMapCoords();
1695 if (evt.type == ev_keyup && evt.keycode == K_F12) {
1696 if (level) toggleFullscreen();
1700 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1701 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1702 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1705 if (evt.type == ev_keydown) {
1706 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1707 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1708 renderMouseTile = evt.bCtrl;
1709 renderMouseRect = evt.bAlt;
1712 if (evt.type == ev_keyup) {
1713 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1714 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1715 renderMouseTile = evt.bCtrl;
1716 renderMouseRect = evt.bAlt;
1719 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1720 int newScale = evt.keycode-48;
1721 if (global.config.scale != newScale) {
1722 global.config.scale = newScale;
1725 cameraTeleportedCB();
1732 if (evt.type == ev_uimouse) {
1733 maskSX = evt.x/global.config.scale;
1734 maskSY = evt.y/global.config.scale;
1737 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1738 maskFrame = max(0, maskFrame-1);
1741 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1742 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1749 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1751 if (saveOptionsDG) saveOptionsDG();
1752 saveOptionsDG = none;
1754 //SoundSystem.UpdateSounds(); // just in case
1755 if (global.hasSpectacles) level.pickedSpectacles();
1758 optionsPane.onEvent(evt);
1762 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1763 if (evt.type == ev_keydown) {
1764 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1765 switch (evt.keycode) {
1766 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1767 case K_F2: if (showHelp != 2) unpauseGame(); return;
1768 case K_F10: GLVideo.requestQuit(); return;
1769 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1773 allowRender = !allowRender;
1779 case K_UPARROW: case K_PAD8:
1780 if (drawStats) statsMoveUp();
1783 case K_DOWNARROW: case K_PAD2:
1784 if (drawStats) statsMoveDown();
1787 case K_LEFTARROW: case K_PAD4:
1788 if (level && showHelp == 2 && level.gameShowHelp) {
1789 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1793 case K_RIGHTARROW: case K_PAD6:
1794 if (level && showHelp == 2 && level.gameShowHelp) {
1795 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1809 resetFramesAndForceOne();
1815 if (/*evt.bCtrl &&*/ showHelp != 2) {
1825 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1826 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1827 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1828 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1829 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1830 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1831 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1832 //case K_j: global.hasJordans = !global.hasJordans; return;
1834 if (/*evt.bCtrl &&*/ showHelp != 2) {
1835 level.resurrectPlayer();
1840 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1841 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1842 if (evt.bAlt && level.player && level.player.dead) {
1843 saveGameSession = false;
1844 replayGameSession = true;
1848 if (/*evt.bCtrl &&*/ showHelp != 2) {
1849 if (evt.bShift) global.idol = false;
1850 level.generateLevel();
1851 level.centerViewAtPlayer();
1852 teleportCameraAt(level.viewStart);
1853 resetFramesAndForceOne();
1857 global.toggleMusic();
1860 if (/*evt.bCtrl &&*/ showHelp != 2) {
1861 foreach (MapTile t; level.allExits) {
1862 if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1863 level.teleportPlayerTo(t.ix+8, t.iy+8);
1871 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1872 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1874 level.teleportPlayerTo(damsel.ix, damsel.iy);
1880 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1884 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1887 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1890 level.teleportPlayerTo(obj.ix, obj.iy-4);
1896 if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1897 if (level && mouseLevelX != int.min) {
1898 //int scale = level.global.scale;
1899 int mapX = mouseLevelX;
1900 int mapY = mouseLevelY;
1901 level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1907 if (evt.bCtrl && showHelp != 2) {
1908 if (level && mouseLevelX != int.min) {
1909 //int scale = level.global.scale;
1910 int mapX = mouseLevelX;
1911 int mapY = mouseLevelY;
1912 level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1918 if (evt.bCtrl && showHelp != 2) {
1919 if (level && mouseLevelX != int.min) {
1920 //int scale = level.global.scale;
1921 int mapX = mouseLevelX;
1922 int mapY = mouseLevelY;
1923 level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1924 level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1930 if (evt.bCtrl && showHelp != 2) {
1931 if (level && mouseLevelX != int.min) {
1932 //int scale = level.global.scale;
1933 int mapX = mouseLevelX;
1934 int mapY = mouseLevelY;
1935 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1939 if (evt.bAlt && showHelp != 2) {
1940 if (level && mouseLevelX != int.min) {
1941 //int scale = level.global.scale;
1942 int mapX = mouseLevelX;
1943 int mapY = mouseLevelY;
1944 level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1950 if (level && mouseLevelX != int.min) {
1951 int scale = level.global.scale;
1952 int mapX = mouseLevelX;
1953 int mapY = mouseLevelY;
1956 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1957 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1958 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1962 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1963 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1969 if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1970 /*auto obj =*/ level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1974 case K_DELETE: // suicide
1975 if (doGameSavingPlaying == Replay.None) {
1976 if (level.player && !level.player.dead && evt.bCtrl) {
1977 global.hasAnkh = false;
1978 level.global.plife = 1;
1979 level.player.invincible = 0;
1980 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1981 if (xplo) xplo.suicide = true;
1988 if (level.player && !level.player.dead && evt.bAlt) {
1989 if (doGameSavingPlaying != Replay.None) {
1990 if (doGameSavingPlaying == Replay.Replaying) {
1992 } else if (doGameSavingPlaying == Replay.Saving) {
1993 saveGameMovement(dbgSessionMovementFileName, packit:true);
1995 doGameSavingPlaying = Replay.None;
1997 saveGameSession = false;
1998 replayGameSession = false;
2005 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
2006 level.stats.setMoneyCheat();
2007 level.stats.addMoney(10000);
2013 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
2014 if (level.player && level.player.dead) {
2015 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
2018 pauseRequested = true;
2023 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2024 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2025 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2028 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2031 if (!level.player || !level.player.dead) {
2032 gameJustOver = false;
2033 } else if (level.player && level.player.dead) {
2034 if (!gameJustOver) {
2036 gameJustOver = true;
2037 waitingForPayRestart = true;
2038 level.clearKeysPressRelease();
2039 if (doGameSavingPlaying == Replay.None) {
2040 stopReplaying(); // just in case
2044 replayFastForward = false;
2045 if (doGameSavingPlaying == Replay.Saving) {
2046 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2047 doGameSavingPlaying = Replay.None;
2048 //clearGameMovement();
2049 saveGameSession = false;
2050 replayGameSession = false;
2053 if (evt.type == ev_keydown || evt.type == ev_keyup) {
2054 bool down = (evt.type == ev_keydown);
2055 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2056 if (down && evt.keycode == K_f) {
2058 if (replayFastForwardSpeed != 4) {
2059 replayFastForwardSpeed = 4;
2060 replayFastForward = true;
2062 replayFastForward = !replayFastForward;
2065 replayFastForwardSpeed = 2;
2066 replayFastForward = !replayFastForward;
2070 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2071 foreach (int kbidx, int kval; global.config.keybinds) {
2072 if (kval && kval == evt.keycode) {
2073 #ifndef BIGGER_REPLAY_DATA
2074 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2076 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2080 if (level.player && level.player.dead) {
2081 if (down && evt.keycode == K_r && evt.bAlt) {
2082 saveGameSession = false;
2083 replayGameSession = true;
2086 if (down && evt.keycode == K_s && evt.bAlt) {
2087 bool wasSaveReq = saveGameSession;
2088 stopReplaying(); // just in case
2089 saveGameSession = !wasSaveReq;
2090 replayGameSession = false;
2093 if (replayGameSession) {
2094 stopReplaying(); // just in case
2095 saveGameSession = false;
2096 replayGameSession = false;
2097 loadGameMovement(dbgSessionMovementFileName);
2098 loadGame(dbgSessionStateFileName);
2099 doGameSavingPlaying = Replay.Replaying;
2102 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2103 if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2104 if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2105 if (waitingForPayRestart) {
2106 level.isKeyReleased(GameConfig::Key.Pay);
2107 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2109 level.isKeyPressed(GameConfig::Key.Pay);
2110 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2111 auto doSave = saveGameSession;
2112 stopReplaying(); // just in case
2113 level.clearKeysPressRelease();
2114 level.restartGame();
2115 level.generateNormalLevel();
2117 saveGameSession = false;
2118 replayGameSession = false;
2119 writeln("DBG: saving game session...");
2120 clearGameMovement();
2121 doGameSavingPlaying = Replay.Saving;
2122 saveGame(dbgSessionStateFileName);
2123 //saveGameMovement(dbgSessionMovementFileName);
2134 void levelExited () {
2140 void closeVideo () {
2141 if (fullscreen && GLVideo.isInitialized) GLVideo.showMouseCursor();
2142 GLVideo.closeScreen();
2146 void initializeVideo () {
2149 if (fullscreen && global.config.fsmode == 1) {
2150 switch (global.config.realfsres) {
2151 case GameConfig::RealFSModes.VM_1024x768: wdt = 1024; hgt = 768; break;
2152 case GameConfig::RealFSModes.VM_1280x960: wdt = 1280; hgt = 960; break;
2153 case GameConfig::RealFSModes.VM_1280x1024: wdt = 1280; hgt = 1024; break;
2154 case GameConfig::RealFSModes.VM_1600x1200: wdt = 1600; hgt = 1200; break;
2155 case GameConfig::RealFSModes.VM_1680x1050: wdt = 1680; hgt = 1050; break;
2156 case GameConfig::RealFSModes.VM_1920x1080: wdt = 1920; hgt = 1080; break;
2157 case GameConfig::RealFSModes.VM_1920x1200: wdt = 1920; hgt = 1200; break;
2160 GLVideo.openScreen("Spelunky/VaVoom C", wdt, hgt, (fullscreen ? global.config.fsmode : 0));
2161 if (GLVideo.realStencilBits < 8) {
2162 GLVideo.closeScreen();
2163 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2166 if (!loserGPU && !GLVideo.framebufferHasAlpha) {
2167 GLVideo.closeScreen();
2168 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!\nRun the game with \"--loser-gpu\" arg if you still want to play.");
2171 if (!GLVideo.framebufferHasAlpha) {
2173 if (level) level.loserGPU = true;
2176 if (!GLVideo.glHasNPOT) {
2177 GLVideo.closeScreen();
2178 FatalError("=== YOUR GPU SUX! ===\nno NPOT texture support!");
2181 if (fullscreen) GLVideo.hideMouseCursor();
2185 void toggleFullscreen () {
2187 fullscreen = !fullscreen;
2192 final void runGameLoop () {
2193 GLVideo.frameTime = 0; // unlimited FPS
2194 lastThinkerTime = 0;
2196 sprStore = SpawnObject(SpriteStore);
2197 sprStore.bDumpLoaded = false;
2199 bgtileStore = SpawnObject(BackTileStore);
2200 bgtileStore.bDumpLoaded = false;
2202 level = SpawnObject(GameLevel);
2203 level.loserGPU = loserGPU;
2204 level.setup(global, sprStore, bgtileStore);
2206 level.BuildYear = BuildYear;
2207 level.BuildMonth = BuildMonth;
2208 level.BuildDay = BuildDay;
2209 level.BuildHour = BuildHour;
2210 level.BuildMin = BuildMin;
2212 level.global = global;
2213 level.sprStore = sprStore;
2214 level.bgtileStore = bgtileStore;
2217 //level.stats.introViewed = 0;
2219 if (level.stats.introViewed == 0) {
2220 startMode = StartMode.Intro;
2221 writeln("FORCED INTRO");
2223 //writeln("INTRO VIWED: ", level.stats.introViewed);
2224 if (level.global.config.skipIntro) startMode = StartMode.Title;
2227 level.onBeforeFrame = &beforeNewFrame;
2228 level.onAfterFrame = &afterNewFrame;
2229 level.onInterFrame = &interFrame;
2230 level.onLevelExitedCB = &levelExited;
2231 level.onCameraTeleported = &cameraTeleportedCB;
2234 maskSX = -0x0ff_fff;
2236 smask = sprStore['sExplosionMask'];
2240 level.viewWidth = 320*3;
2241 level.viewHeight = 240*3;
2243 GLVideo.swapInterval = (global.config.optVSync ? 1 : 0);
2244 //GLVideo.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2245 fullscreen = global.config.startFullscreen;
2248 sprStore.loadFont('sFontSmall');
2250 //SoundSystem.SwapStereo = config.swapStereo;
2251 SoundSystem.NumChannels = 32;
2252 SoundSystem.MaxHearingDistance = 12000;
2253 //SoundSystem.DopplerFactor = 1.0f;
2254 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2255 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2256 SoundSystem.ReferenceDistance = 16.0f*4;
2257 SoundSystem.MaxDistance = 16.0f*(5*10);
2259 SoundSystem.Initialize();
2260 if (!SoundSystem.IsInitialized) {
2261 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2262 global.soundDisabled = true;
2263 global.musicDisabled = true;
2265 global.fixVolumes();
2267 level.restartGame(); // this will NOT generate a new level
2272 texTigerEye = GLTexture.Load("teye0.png");
2274 if (global.cheatEndGameSequence) {
2275 level.winTime = 12*60+42;
2276 level.stats.money = 6666;
2277 switch (global.cheatEndGameSequence) {
2278 case 1: default: level.startWinCutscene(); break;
2279 case 2: level.startWinCutsceneVolcano(); break;
2280 case 3: level.startWinCutsceneWinFall(); break;
2283 switch (startMode) {
2284 case StartMode.Title: level.restartTitle(); break;
2285 case StartMode.Intro: level.restartIntro(); break;
2286 case StartMode.Stars: level.restartStarsRoom(); break;
2287 case StartMode.Sun: level.restartSunRoom(); break;
2288 case StartMode.Moon: level.restartMoonRoom(); break;
2290 level.generateNormalLevel();
2291 if (startMode == StartMode.Dead) {
2292 level.player.dead = true;
2293 level.player.visible = false;
2299 //global.rope = 666;
2300 //global.bombs = 666;
2302 //global.globalRoomSeed = 871520037;
2303 //global.globalOtherSeed = 1047036290;
2305 //level.createTitleRoom();
2306 //level.createTrans4Room();
2307 //level.createOlmecRoom();
2308 //level.generateLevel();
2310 //level.centerViewAtPlayer();
2311 teleportCameraAt(level.viewStart);
2312 //writeln(GLVideo.swapInterval);
2314 GLVideo.runEventLoop();
2316 SoundSystem.Shutdown();
2318 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2326 // ////////////////////////////////////////////////////////////////////////// //
2327 // duplicates are not allowed!
2328 final void checkGameObjNames () {
2329 array!(class!Object) known;
2331 int classCount = 0, namedCount = 0;
2332 foreach AllClasses(Object, out cc) {
2333 auto gn = GetClassGameObjName(cc);
2335 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2336 auto nid = NameToIIndex(gn);
2337 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));
2343 writeln(classCount, " classes, ", namedCount, " game object classes.");
2347 // ////////////////////////////////////////////////////////////////////////// //
2348 #include "timelimit.vc"
2349 //const int TimeLimitDate = 2018232;
2352 void performTimeCheck () {
2353 #ifdef DISABLE_TIME_CHECK
2355 if (TigerEye) return;
2358 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2361 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2363 int tldate = tm.year*1000+tm.yday;
2365 if (tldate > TimeLimitDate) {
2366 level.maxPlayingTime = 24;
2368 //writeln("*** days left: ", TimeLimitDate-tldate);
2374 void setupCheats () {
2377 //level.stats.resetTunnelPrices();
2378 startMode = StartMode.Alive;
2379 global.currLevel = 10;
2380 //global.scumGenAlienCraft = true;
2381 //global.scumGenYetiLair = true;
2384 startMode = StartMode.Alive;
2385 global.currLevel = 8;
2387 level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2388 level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2389 level.stats.tunnel1Active = false;
2390 level.stats.tunnel2Active = false;
2391 level.stats.tunnel3Active = false;
2395 startMode = StartMode.Alive;
2396 global.currLevel = 2;
2397 global.scumGenShop = true;
2398 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2399 //global.config.scale = 1;
2402 startMode = StartMode.Alive;
2403 global.currLevel = 13;
2404 global.config.scale = 2;
2407 startMode = StartMode.Alive;
2408 global.currLevel = 13;
2409 global.config.scale = 1;
2410 global.cityOfGold = true;
2413 startMode = StartMode.Alive;
2414 global.currLevel = 5;
2415 global.genBlackMarket = true;
2418 startMode = StartMode.Alive;
2419 global.currLevel = 2;
2420 global.scumGenShop = true;
2421 global.scumGenShopType = GameGlobal::ShopType.Weapon;
2422 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2423 //global.config.scale = 1;
2426 //startMode = StartMode.Intro;
2429 global.currLevel = 2;
2430 startMode = StartMode.Alive;
2433 global.currLevel = 5;
2434 startMode = StartMode.Alive;
2435 global.scumGenLake = true;
2436 global.config.scale = 1;
2439 startMode = StartMode.Alive;
2440 global.cheatCanSkipOlmec = true;
2441 global.currLevel = 16;
2442 //global.currLevel = 5;
2443 //global.currLevel = 13;
2444 //global.config.scale = 1;
2446 //startMode = StartMode.Dead;
2447 //startMode = StartMode.Title;
2448 //startMode = StartMode.Stars;
2449 //startMode = StartMode.Sun;
2450 startMode = StartMode.Moon;
2452 //global.scumGenSacrificePit = true;
2453 //global.scumAlwaysSacrificeAltar = true;
2455 // first lush jungle level
2456 //global.levelType = 1;
2458 global.scumGenCemetary = true;
2460 //global.idol = false;
2461 //global.currLevel = 5;
2463 //global.isTunnelMan = true;
2466 //global.currLevel = 5;
2467 //global.scumGenLake = true;
2469 //global.currLevel = 5;
2470 //global.currLevel = 9;
2471 //global.currLevel = 13;
2472 //global.currLevel = 14;
2473 //global.cheatEndGameSequence = 1;
2476 //global.currLevel = 6;
2477 global.scumGenAlienCraft = true;
2478 global.currLevel = 9;
2479 //global.scumGenYetiLair = true;
2480 //global.genBlackMarket = true;
2481 //startDead = false;
2482 startMode = StartMode.Alive;
2485 global.cheatCanSkipOlmec = true;
2486 global.currLevel = 15;
2487 startMode = StartMode.Alive;
2490 global.scumGenShop = true;
2491 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2492 global.scumGenShopType = GameGlobal::ShopType.Craps;
2493 //global.scumGenShopType = 6; // craps
2494 //global.scumGenShopType = 7; // kissing
2496 //global.scumAlwaysSacrificeAltar = true;
2500 void setupSeeds () {
2504 // ////////////////////////////////////////////////////////////////////////// //
2505 void main (ref array!string args) {
2506 foreach (string s; args) {
2507 if (s == "--loser-gpu") loserGPU = 1;
2510 checkGameObjNames();
2512 appSetName("k8spelunky");
2513 config = SpawnObject(GameConfig);
2514 global = SpawnObject(GameGlobal);
2515 global.config = config;
2516 config.heroType = GameConfig::Hero.Spelunker;
2518 global.randomizeSeedAll();
2520 fillCheatPickupList();
2521 fillCheatItemsList();
2522 fillCheatEnemiesList();
2525 loadKeyboardBindings();
2527 // force "immediate delete" mode, it is faster
2528 GC_ImmediateDelete = false;
2529 GC_CollectGarbage(true); // destroy delayed objects too
2530 GC_ImmediateDelete = true;