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 = ImmediateDelete;
807 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 = Video.screenWidth;
834 //level.viewHeight = Video.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 ImmediateDelete = olddel;
850 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 = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
933 viewCameraPos.y = round(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 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1116 } else if (col == -999) {
1117 // superfluous collision: blue
1118 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1119 } else if (col <= 0) {
1120 // no collision: yellow
1121 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1122 } else if (col > 0) {
1124 Video.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 Video.color = 0x7f_00_00_ff;
1137 Video.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 Video.color = 0x3f_00_ff_00;
1147 Video.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) Video.color = 0xaf_ff_ff_00;
1165 Video.color = 0xaf_00_ff_00;
1167 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1173 if (frm.bw > 0 && frm.bh > 0) {
1174 setColorByIdx(true, col);
1175 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1176 Video.color = 0xff_00_00;
1177 Video.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 Video.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 Video.color = 0x3f_ff_ff_00;
1289 auto spr = sprStore['sPageDown'];
1290 spr.frames[0].blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1293 Video.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 Video.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 Video.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);
1381 if (Video.frameTime == 0) {
1383 Video.requestRefresh();
1388 if (level.framesProcessedFromLastClear < 1) return;
1389 calcMouseMapCoords();
1391 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1392 Video.clearScreen();
1393 Video.stencil = false;
1394 Video.color = 0xff_ff_ff;
1395 Video.textureFiltering = false;
1396 // don't touch framebuffer alpha
1397 Video.colorMask = Video::CMask.Colors;
1399 Video::ScissorRect scsave;
1400 bool doRestoreGL = false;
1403 if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1405 Video.getScissor(scsave);
1406 Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1407 Video.glPushMatrix();
1408 Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1412 if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1414 float scx = float(Video.screenWidth)/float(level.viewWidth);
1415 float scy = float(Video.screenHeight)/float(level.viewHeight);
1416 float scale = fmin(scx, scy);
1417 int calcedW = trunc(level.viewWidth*scale);
1418 int calcedH = trunc(level.viewHeight*scale);
1419 Video.getScissor(scsave);
1420 int ofsx = (Video.screenWidth-calcedW)/2;
1421 int ofsy = (Video.screenHeight-calcedH)/2;
1422 Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1423 Video.glPushMatrix();
1424 Video.glTranslate(ofsx, ofsy);
1425 Video.glScale(scale, scale);
1428 //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1429 //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1433 level.viewOffsetX = 0;
1434 level.viewOffsetY = 0;
1435 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1438 float scx = float(Video.screenWidth)/float(level.viewWidth);
1439 float scy = float(Video.screenHeight)/float(level.viewHeight);
1440 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1446 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1449 if (level.gamePaused && showHelp != 2) {
1450 if (mouseLevelX != int.min) {
1451 int scale = level.global.scale;
1452 if (renderMouseRect) {
1453 Video.color = 0xcf_ff_ff_00;
1454 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1456 if (renderMouseTile) {
1457 Video.color = 0xaf_ff_00_00;
1458 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1463 switch (doGameSavingPlaying) {
1465 Video.color = 0x7f_00_ff_00;
1466 sprStore.loadFont('sFont');
1467 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1469 case Replay.Replaying:
1470 if (level.player && !level.player.dead) {
1471 Video.color = 0x7f_ff_00_00;
1472 sprStore.loadFont('sFont');
1473 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1474 int th = sprStore.getFontHeight(2);
1475 if (replayFastForward) {
1476 sprStore.loadFont('sFontSmall');
1477 string sstr = va("x%d", replayFastForwardSpeed+1);
1478 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1483 if (saveGameSession) {
1484 Video.color = 0x7f_ff_7f_00;
1485 sprStore.loadFont('sFont');
1486 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1492 if (level.player && level.player.dead && !showHelp) {
1494 Video.color = 0x8f_00_00_00;
1495 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1500 if (true /*level.inWinCutscene == 0*/) {
1501 Video.color = 0xff_ff_ff;
1502 sprStore.loadFont('sFontSmall');
1503 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1505 "PRESS $PAY TO RESTART GAME\n"~
1507 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1509 "TOTAL PLAYING TIME: |%s|"~
1511 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1512 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1513 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1515 GameLevel.time2str(level.stats.playingTime)
1517 kmsg = global.expandString(kmsg);
1518 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1525 Video.color = 0xff_7f_00;
1526 sprStore.loadFont('sFontSmall');
1527 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1528 auto spf = smask.frames[maskFrame];
1529 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1531 spf.bx, spf.by, spf.bw, spf.bh,
1532 (spf.maskEmpty ? "TAN" : "ONA"),
1533 (spf.precise ? "TAN" : "ONA")),
1536 //spf.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1537 //writeln("pos=(", maskSX, ",", maskSY, ")");
1538 int scale = global.config.scale;
1539 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1540 int mapX = xofs/scale+maskSX;
1541 int mapY = yofs/scale+maskSY;
1544 writeln("==== tiles ====");
1546 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1547 if (t.spectral || !t.isInstanceAlive) return false;
1548 Video.color = 0x7f_ff_00_00;
1549 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);
1550 auto tsf = t.getSpriteFrame();
1552 auto spf = smask.frames[maskFrame];
1553 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1554 int mapX = xofs/global.config.scale+maskSX;
1555 int mapY = yofs/global.config.scale+maskSY;
1558 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1559 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1560 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1564 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1565 Video.color = 0x7f_ff_00_00;
1566 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);
1570 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1572 Video.color = 0xaf_ff_ff_ff;
1573 spf.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1574 Video.color = 0xff_ff_00;
1575 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1579 int fx0, fy0, fx1, fy1;
1580 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1581 Video.color = 0x7f_00_00_ff;
1582 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1588 Video.color = 0x8f_00_00_00;
1589 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1591 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1596 Video.color = 0xff_ff_00;
1597 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1598 if (showHelp == 1) {
1599 int msx, msy, ww, wh;
1600 Video.getMousePos(out msx, out msy);
1601 Video.getRealWindowSize(out ww, out wh);
1602 if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1603 sprStore.loadFont('sFontSmall');
1604 Video.color = 0xff_ff_00;
1605 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1606 "F1: show this help\n"~
1608 "K : redefine keys\n"~
1609 "I : toggle interpolaion\n"~
1610 "N : create some blood\n"~
1611 "R : generate a new level\n"~
1612 "F : toggle \"Frozen Area\"\n"~
1613 "X : resurrect player\n"~
1614 "Q : teleport to exit\n"~
1615 "D : teleport to damel\n"~
1617 "C : cheat flags menu\n"~
1618 "P : cheat pickup menu\n"~
1619 "E : cheat enemy menu\n"~
1620 "Enter: cheat items menu\n"~
1622 "TAB: toggle 'freeroam' mode\n"~
1627 if (level) level.renderPauseOverlay();
1631 //SoundSystem.UpdateSounds();
1633 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1636 Video.setScissor(scsave);
1637 Video.glPopMatrix();
1642 Video.color = 0xaf_ff_ff_ff;
1643 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1648 // ////////////////////////////////////////////////////////////////////////// //
1649 transient bool gameJustOver;
1650 transient bool waitingForPayRestart;
1653 final void calcMouseMapCoords () {
1654 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1655 mouseLevelX = int.min;
1656 mouseLevelY = int.min;
1659 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1660 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1661 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1665 final void onEvent (ref event_t evt) {
1666 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1668 if (evt.type == ev_winfocus) {
1669 if (level && !evt.focused) {
1673 //writeln("FOCUS!");
1674 Video.getMousePos(out mouseX, out mouseY);
1679 if (evt.type == ev_mouse) {
1682 calcMouseMapCoords();
1685 if (evt.type == ev_keyup && evt.keycode == K_F12) {
1686 if (level) toggleFullscreen();
1690 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1691 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1692 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1695 if (evt.type == ev_keydown) {
1696 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1697 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1698 renderMouseTile = evt.bCtrl;
1699 renderMouseRect = evt.bAlt;
1702 if (evt.type == ev_keyup) {
1703 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1704 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1705 renderMouseTile = evt.bCtrl;
1706 renderMouseRect = evt.bAlt;
1709 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1710 int newScale = evt.keycode-48;
1711 if (global.config.scale != newScale) {
1712 global.config.scale = newScale;
1715 cameraTeleportedCB();
1722 if (evt.type == ev_mouse) {
1723 maskSX = evt.x/global.config.scale;
1724 maskSY = evt.y/global.config.scale;
1727 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1728 maskFrame = max(0, maskFrame-1);
1731 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1732 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1739 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1741 if (saveOptionsDG) saveOptionsDG();
1742 saveOptionsDG = none;
1744 //SoundSystem.UpdateSounds(); // just in case
1745 if (global.hasSpectacles) level.pickedSpectacles();
1748 optionsPane.onEvent(evt);
1752 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1753 if (evt.type == ev_keydown) {
1754 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1755 switch (evt.keycode) {
1756 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1757 case K_F2: if (showHelp != 2) unpauseGame(); return;
1758 case K_F10: Video.requestQuit(); return;
1759 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1763 allowRender = !allowRender;
1769 case K_UPARROW: case K_PAD8:
1770 if (drawStats) statsMoveUp();
1773 case K_DOWNARROW: case K_PAD2:
1774 if (drawStats) statsMoveDown();
1777 case K_LEFTARROW: case K_PAD4:
1778 if (level && showHelp == 2 && level.gameShowHelp) {
1779 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1783 case K_RIGHTARROW: case K_PAD6:
1784 if (level && showHelp == 2 && level.gameShowHelp) {
1785 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1799 resetFramesAndForceOne();
1805 if (/*evt.bCtrl &&*/ showHelp != 2) {
1815 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1816 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1817 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1818 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1819 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1820 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1821 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1822 //case K_j: global.hasJordans = !global.hasJordans; return;
1824 if (/*evt.bCtrl &&*/ showHelp != 2) {
1825 level.resurrectPlayer();
1830 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1831 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1832 if (evt.bAlt && level.player && level.player.dead) {
1833 saveGameSession = false;
1834 replayGameSession = true;
1838 if (/*evt.bCtrl &&*/ showHelp != 2) {
1839 if (evt.bShift) global.idol = false;
1840 level.generateLevel();
1841 level.centerViewAtPlayer();
1842 teleportCameraAt(level.viewStart);
1843 resetFramesAndForceOne();
1847 global.toggleMusic();
1850 if (/*evt.bCtrl &&*/ showHelp != 2) {
1851 foreach (MapTile t; level.allExits) {
1852 if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1853 level.teleportPlayerTo(t.ix+8, t.iy+8);
1861 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1862 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1864 level.teleportPlayerTo(damsel.ix, damsel.iy);
1870 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1874 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1877 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1880 level.teleportPlayerTo(obj.ix, obj.iy-4);
1886 if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1887 if (level && mouseLevelX != int.min) {
1888 int scale = level.global.scale;
1889 int mapX = mouseLevelX;
1890 int mapY = mouseLevelY;
1891 level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1897 if (evt.bCtrl && showHelp != 2) {
1898 if (level && mouseLevelX != int.min) {
1899 int scale = level.global.scale;
1900 int mapX = mouseLevelX;
1901 int mapY = mouseLevelY;
1902 level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1908 if (evt.bCtrl && showHelp != 2) {
1909 if (level && mouseLevelX != int.min) {
1910 int scale = level.global.scale;
1911 int mapX = mouseLevelX;
1912 int mapY = mouseLevelY;
1913 level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1914 level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1920 if (evt.bCtrl && showHelp != 2) {
1921 if (level && mouseLevelX != int.min) {
1922 int scale = level.global.scale;
1923 int mapX = mouseLevelX;
1924 int mapY = mouseLevelY;
1925 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1929 if (evt.bAlt && showHelp != 2) {
1930 if (level && mouseLevelX != int.min) {
1931 int scale = level.global.scale;
1932 int mapX = mouseLevelX;
1933 int mapY = mouseLevelY;
1934 level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1940 if (level && mouseLevelX != int.min) {
1941 int scale = level.global.scale;
1942 int mapX = mouseLevelX;
1943 int mapY = mouseLevelY;
1946 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1947 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1948 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1952 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1953 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1959 if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1960 auto obj = level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1964 case K_DELETE: // suicide
1965 if (doGameSavingPlaying == Replay.None) {
1966 if (level.player && !level.player.dead && evt.bCtrl) {
1967 global.hasAnkh = false;
1968 level.global.plife = 1;
1969 level.player.invincible = 0;
1970 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1971 if (xplo) xplo.suicide = true;
1978 if (level.player && !level.player.dead && evt.bAlt) {
1979 if (doGameSavingPlaying != Replay.None) {
1980 if (doGameSavingPlaying == Replay.Replaying) {
1982 } else if (doGameSavingPlaying == Replay.Saving) {
1983 saveGameMovement(dbgSessionMovementFileName, packit:true);
1985 doGameSavingPlaying = Replay.None;
1987 saveGameSession = false;
1988 replayGameSession = false;
1995 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1996 level.stats.setMoneyCheat();
1997 level.stats.addMoney(10000);
2003 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
2004 if (level.player && level.player.dead) {
2005 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
2008 pauseRequested = true;
2013 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2014 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2015 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2018 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2021 if (!level.player || !level.player.dead) {
2022 gameJustOver = false;
2023 } else if (level.player && level.player.dead) {
2024 if (!gameJustOver) {
2026 gameJustOver = true;
2027 waitingForPayRestart = true;
2028 level.clearKeysPressRelease();
2029 if (doGameSavingPlaying == Replay.None) {
2030 stopReplaying(); // just in case
2034 replayFastForward = false;
2035 if (doGameSavingPlaying == Replay.Saving) {
2036 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2037 doGameSavingPlaying = Replay.None;
2038 //clearGameMovement();
2039 saveGameSession = false;
2040 replayGameSession = false;
2043 if (evt.type == ev_keydown || evt.type == ev_keyup) {
2044 bool down = (evt.type == ev_keydown);
2045 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2046 if (down && evt.keycode == K_f) {
2048 if (replayFastForwardSpeed != 4) {
2049 replayFastForwardSpeed = 4;
2050 replayFastForward = true;
2052 replayFastForward = !replayFastForward;
2055 replayFastForwardSpeed = 2;
2056 replayFastForward = !replayFastForward;
2060 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2061 foreach (int kbidx, int kval; global.config.keybinds) {
2062 if (kval && kval == evt.keycode) {
2063 #ifndef BIGGER_REPLAY_DATA
2064 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2066 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2070 if (level.player && level.player.dead) {
2071 if (down && evt.keycode == K_r && evt.bAlt) {
2072 saveGameSession = false;
2073 replayGameSession = true;
2076 if (down && evt.keycode == K_s && evt.bAlt) {
2077 bool wasSaveReq = saveGameSession;
2078 stopReplaying(); // just in case
2079 saveGameSession = !wasSaveReq;
2080 replayGameSession = false;
2083 if (replayGameSession) {
2084 stopReplaying(); // just in case
2085 saveGameSession = false;
2086 replayGameSession = false;
2087 loadGameMovement(dbgSessionMovementFileName);
2088 loadGame(dbgSessionStateFileName);
2089 doGameSavingPlaying = Replay.Replaying;
2092 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2093 if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2094 if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2095 if (waitingForPayRestart) {
2096 level.isKeyReleased(GameConfig::Key.Pay);
2097 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2099 level.isKeyPressed(GameConfig::Key.Pay);
2100 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2101 auto doSave = saveGameSession;
2102 stopReplaying(); // just in case
2103 level.clearKeysPressRelease();
2104 level.restartGame();
2105 level.generateNormalLevel();
2107 saveGameSession = false;
2108 replayGameSession = false;
2109 writeln("DBG: saving game session...");
2110 clearGameMovement();
2111 doGameSavingPlaying = Replay.Saving;
2112 saveGame(dbgSessionStateFileName);
2113 //saveGameMovement(dbgSessionMovementFileName);
2124 void levelExited () {
2130 void closeVideo () {
2131 if (fullscreen && Video.isInitialized) Video.showMouseCursor();
2132 Video.closeScreen();
2136 void initializeVideo () {
2139 if (fullscreen && global.config.fsmode == 1) {
2140 switch (global.config.realfsres) {
2141 case GameConfig::RealFSModes.VM_1024x768: wdt = 1024; hgt = 768; break;
2142 case GameConfig::RealFSModes.VM_1280x960: wdt = 1280; hgt = 960; break;
2143 case GameConfig::RealFSModes.VM_1280x1024: wdt = 1280; hgt = 1024; break;
2144 case GameConfig::RealFSModes.VM_1600x1200: wdt = 1600; hgt = 1200; break;
2145 case GameConfig::RealFSModes.VM_1680x1050: wdt = 1680; hgt = 1050; break;
2146 case GameConfig::RealFSModes.VM_1920x1080: wdt = 1920; hgt = 1080; break;
2147 case GameConfig::RealFSModes.VM_1920x1200: wdt = 1920; hgt = 1200; break;
2150 Video.openScreen("Spelunky/VaVoom C", wdt, hgt, (fullscreen ? global.config.fsmode : 0));
2151 if (Video.realStencilBits < 8) {
2152 Video.closeScreen();
2153 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2156 if (!loserGPU && !Video.framebufferHasAlpha) {
2157 Video.closeScreen();
2158 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!\nRun the game with \"--loser-gpu\" arg if you still want to play.");
2161 if (!Video.framebufferHasAlpha) {
2163 if (level) level.loserGPU = true;
2166 if (!Video.glHasNPOT) {
2167 Video.closeScreen();
2168 FatalError("=== YOUR GPU SUX! ===\nno NPOT texture support!");
2171 if (fullscreen) Video.hideMouseCursor();
2175 void toggleFullscreen () {
2177 fullscreen = !fullscreen;
2182 final void runGameLoop () {
2183 Video.frameTime = 0; // unlimited FPS
2184 lastThinkerTime = 0;
2186 sprStore = SpawnObject(SpriteStore);
2187 sprStore.bDumpLoaded = false;
2189 bgtileStore = SpawnObject(BackTileStore);
2190 bgtileStore.bDumpLoaded = false;
2192 level = SpawnObject(GameLevel);
2193 level.loserGPU = loserGPU;
2194 level.setup(global, sprStore, bgtileStore);
2196 level.BuildYear = BuildYear;
2197 level.BuildMonth = BuildMonth;
2198 level.BuildDay = BuildDay;
2199 level.BuildHour = BuildHour;
2200 level.BuildMin = BuildMin;
2202 level.global = global;
2203 level.sprStore = sprStore;
2204 level.bgtileStore = bgtileStore;
2207 //level.stats.introViewed = 0;
2209 if (level.stats.introViewed == 0) {
2210 startMode = StartMode.Intro;
2211 writeln("FORCED INTRO");
2213 //writeln("INTRO VIWED: ", level.stats.introViewed);
2214 if (level.global.config.skipIntro) startMode = StartMode.Title;
2217 level.onBeforeFrame = &beforeNewFrame;
2218 level.onAfterFrame = &afterNewFrame;
2219 level.onInterFrame = &interFrame;
2220 level.onLevelExitedCB = &levelExited;
2221 level.onCameraTeleported = &cameraTeleportedCB;
2224 maskSX = -0x0ff_fff;
2226 smask = sprStore['sExplosionMask'];
2230 level.viewWidth = 320*3;
2231 level.viewHeight = 240*3;
2233 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2234 //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2235 fullscreen = global.config.startFullscreen;
2238 sprStore.loadFont('sFontSmall');
2240 //SoundSystem.SwapStereo = config.swapStereo;
2241 SoundSystem.NumChannels = 32;
2242 SoundSystem.MaxHearingDistance = 12000;
2243 //SoundSystem.DopplerFactor = 1.0f;
2244 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2245 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2246 SoundSystem.ReferenceDistance = 16.0f*4;
2247 SoundSystem.MaxDistance = 16.0f*(5*10);
2249 SoundSystem.Initialize();
2250 if (!SoundSystem.IsInitialized) {
2251 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2252 global.soundDisabled = true;
2253 global.musicDisabled = true;
2255 global.fixVolumes();
2257 level.restartGame(); // this will NOT generate a new level
2262 texTigerEye = GLTexture.Load("teye0.png");
2264 if (global.cheatEndGameSequence) {
2265 level.winTime = 12*60+42;
2266 level.stats.money = 6666;
2267 switch (global.cheatEndGameSequence) {
2268 case 1: default: level.startWinCutscene(); break;
2269 case 2: level.startWinCutsceneVolcano(); break;
2270 case 3: level.startWinCutsceneWinFall(); break;
2273 switch (startMode) {
2274 case StartMode.Title: level.restartTitle(); break;
2275 case StartMode.Intro: level.restartIntro(); break;
2276 case StartMode.Stars: level.restartStarsRoom(); break;
2277 case StartMode.Sun: level.restartSunRoom(); break;
2278 case StartMode.Moon: level.restartMoonRoom(); break;
2280 level.generateNormalLevel();
2281 if (startMode == StartMode.Dead) {
2282 level.player.dead = true;
2283 level.player.visible = false;
2289 //global.rope = 666;
2290 //global.bombs = 666;
2292 //global.globalRoomSeed = 871520037;
2293 //global.globalOtherSeed = 1047036290;
2295 //level.createTitleRoom();
2296 //level.createTrans4Room();
2297 //level.createOlmecRoom();
2298 //level.generateLevel();
2300 //level.centerViewAtPlayer();
2301 teleportCameraAt(level.viewStart);
2302 //writeln(Video.swapInterval);
2304 Video.runEventLoop();
2306 SoundSystem.Shutdown();
2308 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2316 // ////////////////////////////////////////////////////////////////////////// //
2317 // duplicates are not allowed!
2318 final void checkGameObjNames () {
2319 array!(class!Object) known;
2321 int classCount = 0, namedCount = 0;
2322 foreach AllClasses(Object, out cc) {
2323 auto gn = GetClassGameObjName(cc);
2325 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2326 auto nid = NameToInt(gn);
2327 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));
2333 writeln(classCount, " classes, ", namedCount, " game object classes.");
2337 // ////////////////////////////////////////////////////////////////////////// //
2338 #include "timelimit.vc"
2339 //const int TimeLimitDate = 2018232;
2342 void performTimeCheck () {
2343 #ifdef DISABLE_TIME_CHECK
2345 if (TigerEye) return;
2348 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2351 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2353 int tldate = tm.year*1000+tm.yday;
2355 if (tldate > TimeLimitDate) {
2356 level.maxPlayingTime = 24;
2358 //writeln("*** days left: ", TimeLimitDate-tldate);
2364 void setupCheats () {
2367 //level.stats.resetTunnelPrices();
2368 startMode = StartMode.Alive;
2369 global.currLevel = 10;
2370 //global.scumGenAlienCraft = true;
2371 //global.scumGenYetiLair = true;
2374 startMode = StartMode.Alive;
2375 global.currLevel = 8;
2377 level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2378 level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2379 level.stats.tunnel1Active = false;
2380 level.stats.tunnel2Active = false;
2381 level.stats.tunnel3Active = false;
2385 startMode = StartMode.Alive;
2386 global.currLevel = 2;
2387 global.scumGenShop = true;
2388 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2389 //global.config.scale = 1;
2392 startMode = StartMode.Alive;
2393 global.currLevel = 13;
2394 global.config.scale = 2;
2397 startMode = StartMode.Alive;
2398 global.currLevel = 13;
2399 global.config.scale = 1;
2400 global.cityOfGold = true;
2403 startMode = StartMode.Alive;
2404 global.currLevel = 5;
2405 global.genBlackMarket = true;
2408 startMode = StartMode.Alive;
2409 global.currLevel = 2;
2410 global.scumGenShop = true;
2411 global.scumGenShopType = GameGlobal::ShopType.Weapon;
2412 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2413 //global.config.scale = 1;
2416 //startMode = StartMode.Intro;
2419 global.currLevel = 2;
2420 startMode = StartMode.Alive;
2423 global.currLevel = 5;
2424 startMode = StartMode.Alive;
2425 global.scumGenLake = true;
2426 global.config.scale = 1;
2429 startMode = StartMode.Alive;
2430 global.cheatCanSkipOlmec = true;
2431 global.currLevel = 16;
2432 //global.currLevel = 5;
2433 //global.currLevel = 13;
2434 //global.config.scale = 1;
2436 //startMode = StartMode.Dead;
2437 //startMode = StartMode.Title;
2438 //startMode = StartMode.Stars;
2439 //startMode = StartMode.Sun;
2440 startMode = StartMode.Moon;
2442 //global.scumGenSacrificePit = true;
2443 //global.scumAlwaysSacrificeAltar = true;
2445 // first lush jungle level
2446 //global.levelType = 1;
2448 global.scumGenCemetary = true;
2450 //global.idol = false;
2451 //global.currLevel = 5;
2453 //global.isTunnelMan = true;
2456 //global.currLevel = 5;
2457 //global.scumGenLake = true;
2459 //global.currLevel = 5;
2460 //global.currLevel = 9;
2461 //global.currLevel = 13;
2462 //global.currLevel = 14;
2463 //global.cheatEndGameSequence = 1;
2466 //global.currLevel = 6;
2467 global.scumGenAlienCraft = true;
2468 global.currLevel = 9;
2469 //global.scumGenYetiLair = true;
2470 //global.genBlackMarket = true;
2471 //startDead = false;
2472 startMode = StartMode.Alive;
2475 global.cheatCanSkipOlmec = true;
2476 global.currLevel = 15;
2477 startMode = StartMode.Alive;
2480 global.scumGenShop = true;
2481 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2482 global.scumGenShopType = GameGlobal::ShopType.Craps;
2483 //global.scumGenShopType = 6; // craps
2484 //global.scumGenShopType = 7; // kissing
2486 //global.scumAlwaysSacrificeAltar = true;
2490 void setupSeeds () {
2494 // ////////////////////////////////////////////////////////////////////////// //
2495 void main (ref array!string args) {
2496 foreach (string s; args) {
2497 if (s == "--loser-gpu") loserGPU = 1;
2500 checkGameObjNames();
2502 appSetName("k8spelunky");
2503 config = SpawnObject(GameConfig);
2504 global = SpawnObject(GameGlobal);
2505 global.config = config;
2506 config.heroType = GameConfig::Hero.Spelunker;
2508 global.randomizeSeedAll();
2510 fillCheatPickupList();
2511 fillCheatItemsList();
2512 fillCheatEnemiesList();
2515 loadKeyboardBindings();