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) {
521 Video.showMouseCursor();
526 auto fsmode = UIIntEnum.Create(pane, &config.fsmode, 1, 2, "FULLSCREEN MODE: ", "YOU CAN CHOOSE EITHER REAL FULLSCREEN MODE, OR SCALED. USUALLY, SCALED WORKS BETTER, BUT REAL LOOKS NICER (YET IT MAY NOT WORK ON YOUR GPU).");
527 fsmode.names[$] = "REAL";
528 fsmode.names[$] = "SCALED";
529 fsmode.onValueChanged = delegate void (int newval) {
531 Video.showMouseCursor();
538 UILabel.Create(pane, "");
539 UILabel.Create(pane, "HUD OPTIONS");
540 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
541 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.");
542 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
545 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
549 UILabel.Create(pane, "");
550 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
551 //!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.");
552 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
553 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
554 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.");
555 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.");
556 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
557 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.");
558 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
561 UILabel.Create(pane, "");
562 UILabel.Create(pane, "GAMEPLAY OPTIONS");
563 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.");
564 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
565 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
566 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!");
567 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.");
568 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.");
569 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
570 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.");
571 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.");
572 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.");
573 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.");
574 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?");
575 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.");
576 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.");
577 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.");
578 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
579 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
580 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
581 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
582 rstl.names[$] = "RANDOM";
583 rstl.names[$] = "NORMAL";
584 rstl.names[$] = "BIZARRE";
587 UILabel.Create(pane, "");
588 UILabel.Create(pane, "WHIP OPTIONS");
589 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
590 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
591 whiptype.names[$] = "NORMAL";
592 whiptype.names[$] = "LONG";
593 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.");
596 UILabel.Create(pane, "");
597 UILabel.Create(pane, "PLAYER OPTIONS");
598 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
599 herotype.names[$] = "SPELUNKY GUY";
600 herotype.names[$] = "DAMSEL";
601 herotype.names[$] = "TUNNEL MAN";
604 UILabel.Create(pane, "");
605 UILabel.Create(pane, "CHEAT OPTIONS");
606 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.");
607 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
608 plrlit.names[$] = "NEVER";
609 plrlit.names[$] = "FORCED DARKNESS";
610 plrlit.names[$] = "ALWAYS";
611 UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
612 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'.");
613 rdark.names[$] = "NEVER";
614 rdark.names[$] = "DEFAULT";
615 rdark.names[$] = "ALWAYS";
616 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.");
618 rghost.getNameCB = delegate string (int val) {
619 if (val < 0) return "INSTANT";
620 if (val == 0) return "NEVER";
621 if (val < 120) return va("%d SEC", val);
622 if (val%60 == 0) return va("%d MIN", val/60);
623 if (val%60 == 30) return va("%d.5 MIN", val/60);
624 return va("%d MIN, %d SEC", val/60, val%60);
626 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.");
628 UILabel.Create(pane, "");
629 UILabel.Create(pane, "CHEAT START OPTIONS");
630 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.");
631 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
632 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
633 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
634 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
637 UILabel.Create(pane, "");
638 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
639 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
640 mm.names[$] = "SILENCE";
641 mm.names[$] = "RESTART";
642 mm.names[$] = "DON'T TOUCH";
644 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
645 //mm.names[$] = "SILENCE";
646 mm.names[$] = "RESTART";
647 mm.names[$] = "DON'T TOUCH";
650 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
652 swstereo.onValueChanged = delegate void (int newval) {
653 SoundSystem.SwapStereo = newval;
657 UILabel.Create(pane, "");
658 UILabel.Create(pane, "SOUND CONTROL CENTER");
659 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
660 rmusonoff.onValueChanged = delegate void (int newval) {
661 global.restartMusic();
664 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
666 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
667 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
669 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
670 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
673 saveOptionsDG = delegate void () {
674 writeln("saving options");
677 optionsPaneOfs.x = 42;
678 optionsPaneOfs.y = 0;
684 final void createBindingsControl (UIPane pane, int keyidx) {
687 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
688 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
689 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
690 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
691 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
692 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
693 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
694 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
695 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
696 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
697 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
700 int arridx = GameConfig.getKeyIndex(keyidx);
701 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
705 final UIPane createBindingsPane () {
706 UIPane pane = SpawnObject(UIPane);
707 pane.id = 'KeyBindings';
708 pane.sprStore = sprStore;
710 pane.width = 320*3-64;
711 pane.height = 240*3-64;
713 createBindingsControl(pane, GameConfig::Key.Left);
714 createBindingsControl(pane, GameConfig::Key.Right);
715 createBindingsControl(pane, GameConfig::Key.Up);
716 createBindingsControl(pane, GameConfig::Key.Down);
717 createBindingsControl(pane, GameConfig::Key.Jump);
718 createBindingsControl(pane, GameConfig::Key.Run);
719 createBindingsControl(pane, GameConfig::Key.Attack);
720 createBindingsControl(pane, GameConfig::Key.Switch);
721 createBindingsControl(pane, GameConfig::Key.Pay);
722 createBindingsControl(pane, GameConfig::Key.Bomb);
723 createBindingsControl(pane, GameConfig::Key.Rope);
725 saveOptionsDG = delegate void () {
726 writeln("saving keys");
727 saveKeyboardBindings();
729 optionsPaneOfs.x = 120;
730 optionsPaneOfs.y = 140;
736 // ////////////////////////////////////////////////////////////////////////// //
737 void clearGameMovement () {
738 debugMovement = SpawnObject(DebugSessionMovement);
739 debugMovement.playconfig = SpawnObject(GameConfig);
740 debugMovement.playconfig.copyGameplayConfigFrom(config);
741 debugMovement.resetReplay();
745 void saveGameMovement (string fname, optional bool packit) {
746 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
747 saveMovementLastTime = GetTickCount();
751 void loadGameMovement (string fname) {
752 delete debugMovement;
753 debugMovement = appLoadOptions(DebugSessionMovement, fname);
754 debugMovement.resetReplay();
757 origStats = level.stats;
758 origStats.global = none;
759 level.stats = SpawnObject(GameStats);
760 level.stats.global = global;
763 config = debugMovement.playconfig;
764 global.config = config;
765 global.saveSeeds(origSeeds);
770 void stopReplaying () {
772 global.restoreSeeds(origSeeds);
774 delete debugMovement;
775 saveGameSession = false;
776 replayGameSession = false;
777 doGameSavingPlaying = Replay.None;
780 origStats.global = global;
781 level.stats = origStats;
787 global.config = origConfig;
793 // ////////////////////////////////////////////////////////////////////////// //
794 final bool saveGame (string gmname) {
795 return appSaveOptions(level, gmname);
799 final bool loadGame (string gmname) {
800 auto olddel = ImmediateDelete;
801 ImmediateDelete = false;
803 auto stats = level.stats;
806 auto lvl = appLoadOptions(GameLevel, gmname);
808 //lvl.global.config = config;
813 level.loserGPU = loserGPU;
814 global = level.global;
815 global.config = config;
817 level.sprStore = sprStore;
818 level.bgtileStore = bgtileStore;
821 level.onBeforeFrame = &beforeNewFrame;
822 level.onAfterFrame = &afterNewFrame;
823 level.onInterFrame = &interFrame;
824 level.onLevelExitedCB = &levelExited;
825 level.onCameraTeleported = &cameraTeleportedCB;
827 //level.viewWidth = Video.screenWidth;
828 //level.viewHeight = Video.screenHeight;
829 level.viewWidth = 320*3;
830 level.viewHeight = 240*3;
833 level.centerViewAtPlayer();
834 teleportCameraAt(level.viewStart);
836 recalcCameraCoords(0);
841 level.stats.global = level.global;
843 ImmediateDelete = olddel;
844 CollectGarbage(true); // destroy delayed objects too
849 // ////////////////////////////////////////////////////////////////////////// //
850 float lastThinkerTime;
851 int replaySkipFrame = 0;
854 final void onTimePasses () {
855 float curTime = GetTickCount();
856 if (lastThinkerTime > 0) {
857 if (curTime < lastThinkerTime) {
858 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
859 lastThinkerTime = curTime;
862 if (replayFastForward && replaySkipFrame) {
864 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
867 level.processThinkers(curTime-lastThinkerTime);
869 lastThinkerTime = curTime;
873 final void resetFramesAndForceOne () {
874 float curTime = GetTickCount();
875 lastThinkerTime = curTime;
877 auto wasPaused = level.gamePaused;
878 level.gamePaused = false;
879 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
880 level.processThinkers(GameLevel::FrameTime);
881 level.gamePaused = wasPaused;
882 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
886 // ////////////////////////////////////////////////////////////////////////// //
887 private float currFrameDelta; // so level renderer can properly interpolate the player
888 private GameLevel::IVec2D camPrev, camCurr;
889 private GameLevel::IVec2D camShake;
890 private GameLevel::IVec2D viewCameraPos;
893 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
898 viewCameraPos.x = pos.x;
899 viewCameraPos.y = pos.y;
905 // call `recalcCameraCoords()` to get real camera coords after this
906 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
907 // check if camera is moved too far, and teleport it
909 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
910 abs(camCurr.y-pos.y)/global.scale >= 16*4))
912 teleportCameraAt(pos);
914 camPrev.x = camCurr.x;
915 camPrev.y = camCurr.y;
919 camShake.x = level.shakeDir.x*global.scale;
920 camShake.y = level.shakeDir.y*global.scale;
924 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
925 currFrameDelta = frameDelta;
926 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
927 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
929 viewCameraPos.x += camShake.x;
930 viewCameraPos.y += camShake.y;
934 GameLevel::SavedKeyState savedKeyState;
936 final void pauseGame () {
937 if (!level.gamePaused) {
938 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
939 level.gamePaused = true;
940 global.pauseAllSounds();
945 final void unpauseGame () {
946 if (level.gamePaused) {
947 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
948 level.gamePaused = false;
949 level.gameShowHelp = false;
950 level.gameHelpScreen = 0;
951 //lastThinkerTime = 0;
952 global.resumeAllSounds();
954 pauseRequested = false;
955 helpRequested = false;
960 final void beforeNewFrame (bool frameSkip) {
963 level.disablePlayerThink = true;
966 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
967 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
968 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
970 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
971 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
972 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
973 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
975 level.disablePlayerThink = false;
981 if (!level.gamePaused) {
982 // save seeds for afterframe processing
984 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
985 debugMovement.otherSeed = global.globalOtherSeed;
986 debugMovement.roomSeed = global.globalRoomSeed;
990 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
992 #ifdef BIGGER_REPLAY_DATA
993 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
994 debugMovement.keypresses.length += 1;
995 level.keysSaveState(debugMovement.keypresses[$-1]);
996 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
997 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
1001 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
1002 #ifdef BIGGER_REPLAY_DATA
1003 if (debugMovement.keypos < debugMovement.keypresses.length) {
1004 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1005 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1006 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1007 ++debugMovement.keypos;
1013 auto code = debugMovement.getKey(out kbidx, out down);
1014 if (code == DebugSessionMovement::END_OF_RECORD) {
1015 // do this in main loop, so we can view totals
1019 if (code == DebugSessionMovement::END_OF_FRAME) {
1022 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1023 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1031 final void afterNewFrame (bool frameSkip) {
1032 if (!replayFastForward) replaySkipFrame = 0;
1034 if (level.gamePaused) return;
1036 if (!level.gamePaused) {
1037 if (doGameSavingPlaying != Replay.None) {
1038 if (doGameSavingPlaying == Replay.Saving) {
1039 replayFastForward = false; // just in case
1040 #ifndef BIGGER_REPLAY_DATA
1041 debugMovement.addEndOfFrame();
1043 auto stt = GetTickCount();
1044 if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1045 } else if (doGameSavingPlaying == Replay.Replaying) {
1046 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1047 replaySkipFrame = 1;
1053 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1054 //SoundSystem.UpdateSounds();
1056 //if (!freeRide) level.fixCamera();
1057 setNewCameraPos(level.viewStart);
1059 prevCameraX = currCameraX;
1060 prevCameraY = currCameraY;
1061 currCameraX = level.cameraX;
1062 currCameraY = level.cameraY;
1063 // disable camera interpolation if the screen is shaking
1064 if (level.shakeX|level.shakeY) {
1065 prevCameraX = currCameraX;
1066 prevCameraY = currCameraY;
1069 // disable camera interpolation if it moves too far away
1070 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1071 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1073 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1075 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1076 pauseRequested = false;
1078 if (helpRequested) {
1079 helpRequested = false;
1080 level.gameShowHelp = true;
1081 level.gameHelpScreen = 0;
1084 if (!showHelp) showHelp = true;
1086 writeln("active objects in level: ", level.activeItemsCount);
1092 final void interFrame (float frameDelta) {
1093 if (!config.interpolateMovement) return;
1094 recalcCameraCoords(frameDelta);
1098 final void cameraTeleportedCB () {
1099 teleportCameraAt(level.viewStart);
1100 recalcCameraCoords(0);
1104 // ////////////////////////////////////////////////////////////////////////// //
1106 final void setColorByIdx (bool isset, int col) {
1108 // missed collision: red
1109 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1110 } else if (col == -999) {
1111 // superfluous collision: blue
1112 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1113 } else if (col <= 0) {
1114 // no collision: yellow
1115 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1116 } else if (col > 0) {
1118 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1123 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1125 CollisionMask cm = CollisionMask.Create(frm, false);
1127 int scale = global.config.scale;
1128 int bx0, by0, bx1, by1;
1129 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1130 Video.color = 0x7f_00_00_ff;
1131 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1132 if (!cm.isEmptyMask) {
1133 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1134 foreach (int iy; 0..cm.height) {
1135 foreach (int ix; 0..cm.width) {
1136 int v = cm.mask[ix, iy];
1137 foreach (int dx; 0..32) {
1140 Video.color = 0x3f_00_ff_00;
1141 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1150 foreach (int iy; 0..frm.tex.height) {
1151 foreach (int ix; 0..(frm.tex.width+31)/31) {
1152 foreach (int dx; 0..32) {
1154 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1155 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1156 setColorByIdx(true, col);
1157 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1159 Video.color = 0xaf_00_ff_00;
1161 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1167 if (frm.bw > 0 && frm.bh > 0) {
1168 setColorByIdx(true, col);
1169 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1170 Video.color = 0xff_00_00;
1171 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1180 // ////////////////////////////////////////////////////////////////////////// //
1181 transient int drawStats;
1182 transient array!int statsTopItem;
1185 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1186 auto sa = string(a.objName).toUpperCase;
1187 auto sb = string(b.objName).toUpperCase;
1192 final int getStatsTopItem () {
1193 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1197 final void setStatsTopItem (int val) {
1198 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1199 statsTopItem[drawStats] = val;
1203 final void resetStatsTopItem () {
1208 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1209 sprStore.loadFont('sFontSmall');
1215 final int calcStatsVisItems () {
1218 statsDrawGetStartPosLoadFont(currX, currY);
1219 int endY = level.viewHeight-(currY*2);
1220 return max(1, endY/sprStore.getFontHeight(scale));
1224 int getStatsItemCount () {
1225 switch (drawStats) {
1226 case 2: return level.stats.totalKills.length;
1227 case 3: return level.stats.totalDeaths.length;
1228 case 4: return level.stats.totalCollected.length;
1234 final void statsMoveUp () {
1235 int count = getStatsItemCount();
1236 if (count < 0) return;
1237 int visItems = calcStatsVisItems();
1238 if (count <= visItems) { resetStatsTopItem(); return; }
1239 int top = getStatsTopItem();
1241 setStatsTopItem(top-1);
1245 final void statsMoveDown () {
1246 int count = getStatsItemCount();
1247 if (count < 0) return;
1248 int visItems = calcStatsVisItems();
1249 if (count <= visItems) { resetStatsTopItem(); return; }
1250 int top = getStatsTopItem();
1251 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1252 top = clamp(top+1, 0, count-visItems);
1253 setStatsTopItem(top);
1257 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1258 arr.sort(&totalsNameCmpCB);
1262 statsDrawGetStartPosLoadFont(currX, currY);
1264 int endY = level.viewHeight-(currY*2);
1265 int visItems = calcStatsVisItems();
1267 if (arr.length <= visItems) resetStatsTopItem();
1269 int topItem = getStatsTopItem();
1273 Video.color = 0x3f_ff_ff_00;
1274 auto spr = sprStore['sPageUp'];
1275 spr.frames[0].blitAt(currX-28, currY, scale);
1278 // "downscroll" mark
1279 if (topItem+visItems < arr.length) {
1280 Video.color = 0x3f_ff_ff_00;
1281 auto spr = sprStore['sPageDown'];
1282 spr.frames[0].blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1285 Video.color = 0xff_ff_00;
1286 int hiColor = 0x00_ff_00;
1287 int hiColor1 = 0xf_ff_ff;
1290 while (it < arr.length && visItems-- > 0) {
1291 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);
1292 currY += sprStore.getFontHeight(scale);
1298 void drawStatsScreen () {
1299 int deathCount, killCount, collectCount;
1301 sprStore.loadFont('sFontSmall');
1303 Video.color = 0xff_ff_ff;
1304 level.drawTextAtS3Centered(240-2-8, "ESC-RETURN F10-QUIT CTRL+DEL-SUICIDE");
1305 level.drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
1307 Video.color = 0xff_ff_00;
1308 int hiColor = 0x00_ff_00;
1310 switch (drawStats) {
1311 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1312 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1313 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1316 if (drawStats > 1) {
1318 foreach (ref auto i; statsTopItem) i = 0;
1323 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1324 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1325 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1331 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1332 currY += sprStore.getFontHeight(scale);
1334 int gw = level.stats.gamesWon;
1335 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1336 currY += sprStore.getFontHeight(scale);
1338 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1339 currY += sprStore.getFontHeight(scale);
1341 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1342 currY += sprStore.getFontHeight(scale);
1344 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1345 currY += sprStore.getFontHeight(scale);
1347 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1348 currY += sprStore.getFontHeight(scale);
1350 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1351 currY += sprStore.getFontHeight(scale);
1353 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1354 currY += sprStore.getFontHeight(scale);
1356 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1357 currY += sprStore.getFontHeight(scale);
1359 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1360 currY += sprStore.getFontHeight(scale);
1362 int gs = level.stats.totalGhostSummoned;
1363 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1364 currY += sprStore.getFontHeight(scale);
1366 currY += sprStore.getFontHeight(scale);
1367 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1368 currY += sprStore.getFontHeight(scale);
1373 if (Video.frameTime == 0) {
1375 Video.requestRefresh();
1380 if (level.framesProcessedFromLastClear < 1) return;
1381 calcMouseMapCoords();
1383 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1384 Video.clearScreen();
1385 Video.stencil = false;
1386 Video.color = 0xff_ff_ff;
1387 Video.textureFiltering = false;
1388 // don't touch framebuffer alpha
1389 Video.colorMask = Video::CMask.Colors;
1391 Video::ScissorRect scsave;
1392 bool doRestoreGL = false;
1395 if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1397 Video.getScissor(scsave);
1398 Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1399 Video.glPushMatrix();
1400 Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1404 if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1406 float scx = float(Video.screenWidth)/float(level.viewWidth);
1407 float scy = float(Video.screenHeight)/float(level.viewHeight);
1408 float scale = fmin(scx, scy);
1409 int calcedW = trunc(level.viewWidth*scale);
1410 int calcedH = trunc(level.viewHeight*scale);
1411 Video.getScissor(scsave);
1412 int ofsx = (Video.screenWidth-calcedW)/2;
1413 int ofsy = (Video.screenHeight-calcedH)/2;
1414 Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1415 Video.glPushMatrix();
1416 Video.glTranslate(ofsx, ofsy);
1417 Video.glScale(scale, scale);
1420 //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1421 //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1425 level.viewOffsetX = 0;
1426 level.viewOffsetY = 0;
1427 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1430 float scx = float(Video.screenWidth)/float(level.viewWidth);
1431 float scy = float(Video.screenHeight)/float(level.viewHeight);
1432 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1438 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1441 if (level.gamePaused && showHelp != 2) {
1442 if (mouseLevelX != int.min) {
1443 int scale = level.global.scale;
1444 if (renderMouseRect) {
1445 Video.color = 0xcf_ff_ff_00;
1446 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1448 if (renderMouseTile) {
1449 Video.color = 0xaf_ff_00_00;
1450 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1455 switch (doGameSavingPlaying) {
1457 Video.color = 0x7f_00_ff_00;
1458 sprStore.loadFont('sFont');
1459 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1461 case Replay.Replaying:
1462 if (level.player && !level.player.dead) {
1463 Video.color = 0x7f_ff_00_00;
1464 sprStore.loadFont('sFont');
1465 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1466 int th = sprStore.getFontHeight(2);
1467 if (replayFastForward) {
1468 sprStore.loadFont('sFontSmall');
1469 string sstr = va("x%d", replayFastForwardSpeed+1);
1470 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1475 if (saveGameSession) {
1476 Video.color = 0x7f_ff_7f_00;
1477 sprStore.loadFont('sFont');
1478 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1484 if (level.player && level.player.dead && !showHelp) {
1486 Video.color = 0x8f_00_00_00;
1487 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1492 if (true /*level.inWinCutscene == 0*/) {
1493 Video.color = 0xff_ff_ff;
1494 sprStore.loadFont('sFontSmall');
1495 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1497 "PRESS $PAY TO RESTART GAME\n"~
1499 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1501 "TOTAL PLAYING TIME: |%s|"~
1503 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1504 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1505 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1507 GameLevel.time2str(level.stats.playingTime)
1509 kmsg = global.expandString(kmsg);
1510 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1517 Video.color = 0xff_7f_00;
1518 sprStore.loadFont('sFontSmall');
1519 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1520 auto spf = smask.frames[maskFrame];
1521 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1523 spf.bx, spf.by, spf.bw, spf.bh,
1524 (spf.maskEmpty ? "TAN" : "ONA"),
1525 (spf.precise ? "TAN" : "ONA")),
1528 //spf.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1529 //writeln("pos=(", maskSX, ",", maskSY, ")");
1530 int scale = global.config.scale;
1531 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1532 int mapX = xofs/scale+maskSX;
1533 int mapY = yofs/scale+maskSY;
1536 writeln("==== tiles ====");
1538 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1539 if (t.spectral || !t.isInstanceAlive) return false;
1540 Video.color = 0x7f_ff_00_00;
1541 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);
1542 auto tsf = t.getSpriteFrame();
1544 auto spf = smask.frames[maskFrame];
1545 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1546 int mapX = xofs/global.config.scale+maskSX;
1547 int mapY = yofs/global.config.scale+maskSY;
1550 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1551 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1552 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1556 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1557 Video.color = 0x7f_ff_00_00;
1558 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);
1562 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1564 Video.color = 0xaf_ff_ff_ff;
1565 spf.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1566 Video.color = 0xff_ff_00;
1567 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1571 int fx0, fy0, fx1, fy1;
1572 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1573 Video.color = 0x7f_00_00_ff;
1574 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1580 Video.color = 0x8f_00_00_00;
1581 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1583 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1588 Video.color = 0xff_ff_00;
1589 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1590 if (showHelp == 1) {
1591 int msx, msy, ww, wh;
1592 Video.getMousePos(out msx, out msy);
1593 Video.getRealWindowSize(out ww, out wh);
1594 if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1595 sprStore.loadFont('sFontSmall');
1596 Video.color = 0xff_ff_00;
1597 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1598 "F1: show this help\n"~
1600 "K : redefine keys\n"~
1601 "I : toggle interpolaion\n"~
1602 "N : create some blood\n"~
1603 "R : generate a new level\n"~
1604 "F : toggle \"Frozen Area\"\n"~
1605 "X : resurrect player\n"~
1606 "Q : teleport to exit\n"~
1607 "D : teleport to damel\n"~
1609 "C : cheat flags menu\n"~
1610 "P : cheat pickup menu\n"~
1611 "E : cheat enemy menu\n"~
1612 "Enter: cheat items menu\n"~
1614 "TAB: toggle 'freeroam' mode\n"~
1619 if (level) level.renderPauseOverlay();
1623 //SoundSystem.UpdateSounds();
1625 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1628 Video.setScissor(scsave);
1629 Video.glPopMatrix();
1634 Video.color = 0xaf_ff_ff_ff;
1635 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1640 // ////////////////////////////////////////////////////////////////////////// //
1641 transient bool gameJustOver;
1642 transient bool waitingForPayRestart;
1645 final void calcMouseMapCoords () {
1646 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1647 mouseLevelX = int.min;
1648 mouseLevelY = int.min;
1651 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1652 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1653 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1657 final void onEvent (ref event_t evt) {
1658 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1660 if (evt.type == ev_winfocus) {
1661 if (level && !evt.focused) {
1665 //writeln("FOCUS!");
1666 Video.getMousePos(out mouseX, out mouseY);
1671 if (evt.type == ev_mouse) {
1674 calcMouseMapCoords();
1677 if (evt.type == ev_keydown && evt.keycode == K_F12) {
1678 if (level) toggleFullscreen();
1682 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1683 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1684 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1687 if (evt.type == ev_keydown) {
1688 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1689 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1690 renderMouseTile = evt.bCtrl;
1691 renderMouseRect = evt.bAlt;
1694 if (evt.type == ev_keyup) {
1695 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1696 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1697 renderMouseTile = evt.bCtrl;
1698 renderMouseRect = evt.bAlt;
1701 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1702 int newScale = evt.keycode-48;
1703 if (global.config.scale != newScale) {
1704 global.config.scale = newScale;
1707 cameraTeleportedCB();
1714 if (evt.type == ev_mouse) {
1715 maskSX = evt.x/global.config.scale;
1716 maskSY = evt.y/global.config.scale;
1719 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1720 maskFrame = max(0, maskFrame-1);
1723 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1724 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1731 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1733 if (saveOptionsDG) saveOptionsDG();
1734 saveOptionsDG = none;
1736 //SoundSystem.UpdateSounds(); // just in case
1737 if (global.hasSpectacles) level.pickedSpectacles();
1740 optionsPane.onEvent(evt);
1744 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1745 if (evt.type == ev_keydown) {
1746 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1747 switch (evt.keycode) {
1748 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1749 case K_F2: if (showHelp != 2) unpauseGame(); return;
1750 case K_F10: Video.requestQuit(); return;
1751 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1755 allowRender = !allowRender;
1761 case K_UPARROW: case K_PAD8:
1762 if (drawStats) statsMoveUp();
1765 case K_DOWNARROW: case K_PAD2:
1766 if (drawStats) statsMoveDown();
1769 case K_LEFTARROW: case K_PAD4:
1770 if (level && showHelp == 2 && level.gameShowHelp) {
1771 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1775 case K_RIGHTARROW: case K_PAD6:
1776 if (level && showHelp == 2 && level.gameShowHelp) {
1777 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1791 resetFramesAndForceOne();
1797 if (/*evt.bCtrl &&*/ showHelp != 2) {
1807 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1808 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1809 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1810 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1811 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1812 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1813 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1814 //case K_j: global.hasJordans = !global.hasJordans; return;
1816 if (/*evt.bCtrl &&*/ showHelp != 2) {
1817 level.resurrectPlayer();
1822 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1823 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1824 if (evt.bAlt && level.player && level.player.dead) {
1825 saveGameSession = false;
1826 replayGameSession = true;
1830 if (/*evt.bCtrl &&*/ showHelp != 2) {
1831 if (evt.bShift) global.idol = false;
1832 level.generateLevel();
1833 level.centerViewAtPlayer();
1834 teleportCameraAt(level.viewStart);
1835 resetFramesAndForceOne();
1839 global.toggleMusic();
1842 if (/*evt.bCtrl &&*/ showHelp != 2) {
1843 foreach (MapTile t; level.allExits) {
1844 if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1845 level.teleportPlayerTo(t.ix+8, t.iy+8);
1853 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1854 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1856 level.teleportPlayerTo(damsel.ix, damsel.iy);
1862 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1866 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1869 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1872 level.teleportPlayerTo(obj.ix, obj.iy-4);
1878 if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1879 if (level && mouseLevelX != int.min) {
1880 int scale = level.global.scale;
1881 int mapX = mouseLevelX;
1882 int mapY = mouseLevelY;
1883 level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1889 if (evt.bCtrl && showHelp != 2) {
1890 if (level && mouseLevelX != int.min) {
1891 int scale = level.global.scale;
1892 int mapX = mouseLevelX;
1893 int mapY = mouseLevelY;
1894 level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1900 if (evt.bCtrl && showHelp != 2) {
1901 if (level && mouseLevelX != int.min) {
1902 int scale = level.global.scale;
1903 int mapX = mouseLevelX;
1904 int mapY = mouseLevelY;
1905 level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1906 level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1912 if (evt.bCtrl && showHelp != 2) {
1913 if (level && mouseLevelX != int.min) {
1914 int scale = level.global.scale;
1915 int mapX = mouseLevelX;
1916 int mapY = mouseLevelY;
1917 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1921 if (evt.bAlt && showHelp != 2) {
1922 if (level && mouseLevelX != int.min) {
1923 int scale = level.global.scale;
1924 int mapX = mouseLevelX;
1925 int mapY = mouseLevelY;
1926 level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1932 if (level && mouseLevelX != int.min) {
1933 int scale = level.global.scale;
1934 int mapX = mouseLevelX;
1935 int mapY = mouseLevelY;
1938 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1939 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1940 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1944 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1945 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1951 if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1952 auto obj = level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1956 case K_DELETE: // suicide
1957 if (doGameSavingPlaying == Replay.None) {
1958 if (level.player && !level.player.dead && evt.bCtrl) {
1959 global.hasAnkh = false;
1960 level.global.plife = 1;
1961 level.player.invincible = 0;
1962 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1963 if (xplo) xplo.suicide = true;
1970 if (level.player && !level.player.dead && evt.bAlt) {
1971 if (doGameSavingPlaying != Replay.None) {
1972 if (doGameSavingPlaying == Replay.Replaying) {
1974 } else if (doGameSavingPlaying == Replay.Saving) {
1975 saveGameMovement(dbgSessionMovementFileName, packit:true);
1977 doGameSavingPlaying = Replay.None;
1979 saveGameSession = false;
1980 replayGameSession = false;
1987 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1988 level.stats.setMoneyCheat();
1989 level.stats.addMoney(10000);
1995 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1996 if (level.player && level.player.dead) {
1997 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
2000 pauseRequested = true;
2005 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2006 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2007 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2010 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2013 if (!level.player || !level.player.dead) {
2014 gameJustOver = false;
2015 } else if (level.player && level.player.dead) {
2016 if (!gameJustOver) {
2018 gameJustOver = true;
2019 waitingForPayRestart = true;
2020 level.clearKeysPressRelease();
2021 if (doGameSavingPlaying == Replay.None) {
2022 stopReplaying(); // just in case
2026 replayFastForward = false;
2027 if (doGameSavingPlaying == Replay.Saving) {
2028 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2029 doGameSavingPlaying = Replay.None;
2030 //clearGameMovement();
2031 saveGameSession = false;
2032 replayGameSession = false;
2035 if (evt.type == ev_keydown || evt.type == ev_keyup) {
2036 bool down = (evt.type == ev_keydown);
2037 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2038 if (down && evt.keycode == K_f) {
2040 if (replayFastForwardSpeed != 4) {
2041 replayFastForwardSpeed = 4;
2042 replayFastForward = true;
2044 replayFastForward = !replayFastForward;
2047 replayFastForwardSpeed = 2;
2048 replayFastForward = !replayFastForward;
2052 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2053 foreach (int kbidx, int kval; global.config.keybinds) {
2054 if (kval && kval == evt.keycode) {
2055 #ifndef BIGGER_REPLAY_DATA
2056 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2058 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2062 if (level.player && level.player.dead) {
2063 if (down && evt.keycode == K_r && evt.bAlt) {
2064 saveGameSession = false;
2065 replayGameSession = true;
2068 if (down && evt.keycode == K_s && evt.bAlt) {
2069 bool wasSaveReq = saveGameSession;
2070 stopReplaying(); // just in case
2071 saveGameSession = !wasSaveReq;
2072 replayGameSession = false;
2075 if (replayGameSession) {
2076 stopReplaying(); // just in case
2077 saveGameSession = false;
2078 replayGameSession = false;
2079 loadGameMovement(dbgSessionMovementFileName);
2080 loadGame(dbgSessionStateFileName);
2081 doGameSavingPlaying = Replay.Replaying;
2084 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2085 if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2086 if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2087 if (waitingForPayRestart) {
2088 level.isKeyReleased(GameConfig::Key.Pay);
2089 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2091 level.isKeyPressed(GameConfig::Key.Pay);
2092 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2093 auto doSave = saveGameSession;
2094 stopReplaying(); // just in case
2095 level.clearKeysPressRelease();
2096 level.restartGame();
2097 level.generateNormalLevel();
2099 saveGameSession = false;
2100 replayGameSession = false;
2101 writeln("DBG: saving game session...");
2102 clearGameMovement();
2103 doGameSavingPlaying = Replay.Saving;
2104 saveGame(dbgSessionStateFileName);
2105 //saveGameMovement(dbgSessionMovementFileName);
2116 void levelExited () {
2122 void initializeVideo () {
2123 Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2124 if (Video.realStencilBits < 8) {
2125 Video.closeScreen();
2126 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2129 if (!loserGPU && !Video.framebufferHasAlpha) {
2130 Video.closeScreen();
2131 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!\nRun the game with \"--loser-gpu\" arg if you still want to play.");
2134 if (!Video.framebufferHasAlpha) {
2136 if (level) level.loserGPU = true;
2139 if (!Video.glHasNPOT) {
2140 Video.closeScreen();
2141 FatalError("=== YOUR GPU SUX! ===\nno NPOT texture support!");
2144 if (fullscreen) Video.hideMouseCursor();
2148 void toggleFullscreen () {
2149 Video.showMouseCursor();
2150 Video.closeScreen();
2151 fullscreen = !fullscreen;
2156 final void runGameLoop () {
2157 Video.frameTime = 0; // unlimited FPS
2158 lastThinkerTime = 0;
2160 sprStore = SpawnObject(SpriteStore);
2161 sprStore.bDumpLoaded = false;
2163 bgtileStore = SpawnObject(BackTileStore);
2164 bgtileStore.bDumpLoaded = false;
2166 level = SpawnObject(GameLevel);
2167 level.loserGPU = loserGPU;
2168 level.setup(global, sprStore, bgtileStore);
2170 level.BuildYear = BuildYear;
2171 level.BuildMonth = BuildMonth;
2172 level.BuildDay = BuildDay;
2173 level.BuildHour = BuildHour;
2174 level.BuildMin = BuildMin;
2176 level.global = global;
2177 level.sprStore = sprStore;
2178 level.bgtileStore = bgtileStore;
2181 //level.stats.introViewed = 0;
2183 if (level.stats.introViewed == 0) {
2184 startMode = StartMode.Intro;
2185 writeln("FORCED INTRO");
2187 //writeln("INTRO VIWED: ", level.stats.introViewed);
2188 if (level.global.config.skipIntro) startMode = StartMode.Title;
2191 level.onBeforeFrame = &beforeNewFrame;
2192 level.onAfterFrame = &afterNewFrame;
2193 level.onInterFrame = &interFrame;
2194 level.onLevelExitedCB = &levelExited;
2195 level.onCameraTeleported = &cameraTeleportedCB;
2198 maskSX = -0x0ff_fff;
2200 smask = sprStore['sExplosionMask'];
2204 level.viewWidth = 320*3;
2205 level.viewHeight = 240*3;
2207 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2208 //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2209 fullscreen = global.config.startFullscreen;
2212 sprStore.loadFont('sFontSmall');
2214 //SoundSystem.SwapStereo = config.swapStereo;
2215 SoundSystem.NumChannels = 32;
2216 SoundSystem.MaxHearingDistance = 12000;
2217 //SoundSystem.DopplerFactor = 1.0f;
2218 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2219 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2220 SoundSystem.ReferenceDistance = 16.0f*4;
2221 SoundSystem.MaxDistance = 16.0f*(5*10);
2223 SoundSystem.Initialize();
2224 if (!SoundSystem.IsInitialized) {
2225 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2226 global.soundDisabled = true;
2227 global.musicDisabled = true;
2229 global.fixVolumes();
2231 level.restartGame(); // this will NOT generate a new level
2236 texTigerEye = GLTexture.Load("teye0.png");
2238 if (global.cheatEndGameSequence) {
2239 level.winTime = 12*60+42;
2240 level.stats.money = 6666;
2241 switch (global.cheatEndGameSequence) {
2242 case 1: default: level.startWinCutscene(); break;
2243 case 2: level.startWinCutsceneVolcano(); break;
2244 case 3: level.startWinCutsceneWinFall(); break;
2247 switch (startMode) {
2248 case StartMode.Title: level.restartTitle(); break;
2249 case StartMode.Intro: level.restartIntro(); break;
2250 case StartMode.Stars: level.restartStarsRoom(); break;
2251 case StartMode.Sun: level.restartSunRoom(); break;
2252 case StartMode.Moon: level.restartMoonRoom(); break;
2254 level.generateNormalLevel();
2255 if (startMode == StartMode.Dead) {
2256 level.player.dead = true;
2257 level.player.visible = false;
2263 //global.rope = 666;
2264 //global.bombs = 666;
2266 //global.globalRoomSeed = 871520037;
2267 //global.globalOtherSeed = 1047036290;
2269 //level.createTitleRoom();
2270 //level.createTrans4Room();
2271 //level.createOlmecRoom();
2272 //level.generateLevel();
2274 //level.centerViewAtPlayer();
2275 teleportCameraAt(level.viewStart);
2276 //writeln(Video.swapInterval);
2278 Video.runEventLoop();
2279 Video.showMouseCursor();
2280 Video.closeScreen();
2281 SoundSystem.Shutdown();
2283 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2291 // ////////////////////////////////////////////////////////////////////////// //
2292 // duplicates are not allowed!
2293 final void checkGameObjNames () {
2294 array!(class!Object) known;
2296 int classCount = 0, namedCount = 0;
2297 foreach AllClasses(Object, out cc) {
2298 auto gn = GetClassGameObjName(cc);
2300 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2301 auto nid = NameToInt(gn);
2302 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));
2308 writeln(classCount, " classes, ", namedCount, " game object classes.");
2312 // ////////////////////////////////////////////////////////////////////////// //
2313 #include "timelimit.vc"
2314 //const int TimeLimitDate = 2018232;
2317 void performTimeCheck () {
2318 #ifdef DISABLE_TIME_CHECK
2321 if (TigerEye) return;
2324 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2327 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2329 int tldate = tm.year*1000+tm.yday;
2331 if (tldate > TimeLimitDate) {
2332 level.maxPlayingTime = 24;
2334 //writeln("*** days left: ", TimeLimitDate-tldate);
2340 void setupCheats () {
2343 //level.stats.resetTunnelPrices();
2344 startMode = StartMode.Alive;
2345 global.currLevel = 10;
2346 //global.scumGenAlienCraft = true;
2347 //global.scumGenYetiLair = true;
2350 startMode = StartMode.Alive;
2351 global.currLevel = 8;
2353 level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2354 level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2355 level.stats.tunnel1Active = false;
2356 level.stats.tunnel2Active = false;
2357 level.stats.tunnel3Active = false;
2361 startMode = StartMode.Alive;
2362 global.currLevel = 2;
2363 global.scumGenShop = true;
2364 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2365 //global.config.scale = 1;
2368 startMode = StartMode.Alive;
2369 global.currLevel = 13;
2370 global.config.scale = 2;
2373 startMode = StartMode.Alive;
2374 global.currLevel = 13;
2375 global.config.scale = 1;
2376 global.cityOfGold = true;
2379 startMode = StartMode.Alive;
2380 global.currLevel = 5;
2381 global.genBlackMarket = true;
2384 startMode = StartMode.Alive;
2385 global.currLevel = 2;
2386 global.scumGenShop = true;
2387 global.scumGenShopType = GameGlobal::ShopType.Weapon;
2388 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2389 //global.config.scale = 1;
2392 //startMode = StartMode.Intro;
2395 global.currLevel = 2;
2396 startMode = StartMode.Alive;
2399 global.currLevel = 5;
2400 startMode = StartMode.Alive;
2401 global.scumGenLake = true;
2402 global.config.scale = 1;
2405 startMode = StartMode.Alive;
2406 global.cheatCanSkipOlmec = true;
2407 global.currLevel = 16;
2408 //global.currLevel = 5;
2409 //global.currLevel = 13;
2410 //global.config.scale = 1;
2412 //startMode = StartMode.Dead;
2413 //startMode = StartMode.Title;
2414 //startMode = StartMode.Stars;
2415 //startMode = StartMode.Sun;
2416 startMode = StartMode.Moon;
2418 //global.scumGenSacrificePit = true;
2419 //global.scumAlwaysSacrificeAltar = true;
2421 // first lush jungle level
2422 //global.levelType = 1;
2424 global.scumGenCemetary = true;
2426 //global.idol = false;
2427 //global.currLevel = 5;
2429 //global.isTunnelMan = true;
2432 //global.currLevel = 5;
2433 //global.scumGenLake = true;
2435 //global.currLevel = 5;
2436 //global.currLevel = 9;
2437 //global.currLevel = 13;
2438 //global.currLevel = 14;
2439 //global.cheatEndGameSequence = 1;
2442 //global.currLevel = 6;
2443 global.scumGenAlienCraft = true;
2444 global.currLevel = 9;
2445 //global.scumGenYetiLair = true;
2446 //global.genBlackMarket = true;
2447 //startDead = false;
2448 startMode = StartMode.Alive;
2451 global.cheatCanSkipOlmec = true;
2452 global.currLevel = 15;
2453 startMode = StartMode.Alive;
2456 global.scumGenShop = true;
2457 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2458 global.scumGenShopType = GameGlobal::ShopType.Craps;
2459 //global.scumGenShopType = 6; // craps
2460 //global.scumGenShopType = 7; // kissing
2462 //global.scumAlwaysSacrificeAltar = true;
2466 void setupSeeds () {
2470 // ////////////////////////////////////////////////////////////////////////// //
2471 void main (ref array!string args) {
2472 foreach (string s; args) {
2473 if (s == "--loser-gpu") loserGPU = 1;
2476 checkGameObjNames();
2478 appSetName("k8spelunky");
2479 config = SpawnObject(GameConfig);
2480 global = SpawnObject(GameGlobal);
2481 global.config = config;
2482 config.heroType = GameConfig::Hero.Spelunker;
2484 global.randomizeSeedAll();
2486 fillCheatPickupList();
2487 fillCheatItemsList();
2488 fillCheatEnemiesList();
2491 loadKeyboardBindings();