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, BUT REAL LOOKS NICER (YET IT MAY NOT WORK ON YOUR GPU).");
526 fsmode.names[$] = "REAL";
527 fsmode.names[$] = "SCALED";
528 fsmode.onValueChanged = delegate void (int newval) {
536 UILabel.Create(pane, "");
537 UILabel.Create(pane, "HUD OPTIONS");
538 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
539 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.");
540 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
543 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
547 UILabel.Create(pane, "");
548 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
549 //!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.");
550 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
551 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
552 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.");
553 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.");
554 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
555 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.");
556 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
559 UILabel.Create(pane, "");
560 UILabel.Create(pane, "GAMEPLAY OPTIONS");
561 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.");
562 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
563 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
564 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!");
565 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.");
566 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.");
567 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
568 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.");
569 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.");
570 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.");
571 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.");
572 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?");
573 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.");
574 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.");
575 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.");
576 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
577 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
578 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
579 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
580 rstl.names[$] = "RANDOM";
581 rstl.names[$] = "NORMAL";
582 rstl.names[$] = "BIZARRE";
585 UILabel.Create(pane, "");
586 UILabel.Create(pane, "WHIP OPTIONS");
587 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
588 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
589 whiptype.names[$] = "NORMAL";
590 whiptype.names[$] = "LONG";
591 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.");
594 UILabel.Create(pane, "");
595 UILabel.Create(pane, "PLAYER OPTIONS");
596 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
597 herotype.names[$] = "SPELUNKY GUY";
598 herotype.names[$] = "DAMSEL";
599 herotype.names[$] = "TUNNEL MAN";
602 UILabel.Create(pane, "");
603 UILabel.Create(pane, "CHEAT OPTIONS");
604 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.");
605 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
606 plrlit.names[$] = "NEVER";
607 plrlit.names[$] = "FORCED DARKNESS";
608 plrlit.names[$] = "ALWAYS";
609 UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
610 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'.");
611 rdark.names[$] = "NEVER";
612 rdark.names[$] = "DEFAULT";
613 rdark.names[$] = "ALWAYS";
614 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.");
616 rghost.getNameCB = delegate string (int val) {
617 if (val < 0) return "INSTANT";
618 if (val == 0) return "NEVER";
619 if (val < 120) return va("%d SEC", val);
620 if (val%60 == 0) return va("%d MIN", val/60);
621 if (val%60 == 30) return va("%d.5 MIN", val/60);
622 return va("%d MIN, %d SEC", val/60, val%60);
624 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.");
626 UILabel.Create(pane, "");
627 UILabel.Create(pane, "CHEAT START OPTIONS");
628 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.");
629 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
630 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
631 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
632 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
635 UILabel.Create(pane, "");
636 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
637 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
638 mm.names[$] = "SILENCE";
639 mm.names[$] = "RESTART";
640 mm.names[$] = "DON'T TOUCH";
642 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
643 //mm.names[$] = "SILENCE";
644 mm.names[$] = "RESTART";
645 mm.names[$] = "DON'T TOUCH";
648 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
650 swstereo.onValueChanged = delegate void (int newval) {
651 SoundSystem.SwapStereo = newval;
655 UILabel.Create(pane, "");
656 UILabel.Create(pane, "SOUND CONTROL CENTER");
657 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
658 rmusonoff.onValueChanged = delegate void (int newval) {
659 global.restartMusic();
662 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
664 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
665 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
667 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
668 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
671 saveOptionsDG = delegate void () {
672 writeln("saving options");
675 optionsPaneOfs.x = 42;
676 optionsPaneOfs.y = 0;
682 final void createBindingsControl (UIPane pane, int keyidx) {
685 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
686 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
687 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
688 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
689 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
690 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
691 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
692 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
693 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
694 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
695 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
698 int arridx = GameConfig.getKeyIndex(keyidx);
699 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
703 final UIPane createBindingsPane () {
704 UIPane pane = SpawnObject(UIPane);
705 pane.id = 'KeyBindings';
706 pane.sprStore = sprStore;
708 pane.width = 320*3-64;
709 pane.height = 240*3-64;
711 createBindingsControl(pane, GameConfig::Key.Left);
712 createBindingsControl(pane, GameConfig::Key.Right);
713 createBindingsControl(pane, GameConfig::Key.Up);
714 createBindingsControl(pane, GameConfig::Key.Down);
715 createBindingsControl(pane, GameConfig::Key.Jump);
716 createBindingsControl(pane, GameConfig::Key.Run);
717 createBindingsControl(pane, GameConfig::Key.Attack);
718 createBindingsControl(pane, GameConfig::Key.Switch);
719 createBindingsControl(pane, GameConfig::Key.Pay);
720 createBindingsControl(pane, GameConfig::Key.Bomb);
721 createBindingsControl(pane, GameConfig::Key.Rope);
723 saveOptionsDG = delegate void () {
724 writeln("saving keys");
725 saveKeyboardBindings();
727 optionsPaneOfs.x = 120;
728 optionsPaneOfs.y = 140;
734 // ////////////////////////////////////////////////////////////////////////// //
735 void clearGameMovement () {
736 debugMovement = SpawnObject(DebugSessionMovement);
737 debugMovement.playconfig = SpawnObject(GameConfig);
738 debugMovement.playconfig.copyGameplayConfigFrom(config);
739 debugMovement.resetReplay();
743 void saveGameMovement (string fname, optional bool packit) {
744 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
745 saveMovementLastTime = GetTickCount();
749 void loadGameMovement (string fname) {
750 delete debugMovement;
751 debugMovement = appLoadOptions(DebugSessionMovement, fname);
752 debugMovement.resetReplay();
755 origStats = level.stats;
756 origStats.global = none;
757 level.stats = SpawnObject(GameStats);
758 level.stats.global = global;
761 config = debugMovement.playconfig;
762 global.config = config;
763 global.saveSeeds(origSeeds);
768 void stopReplaying () {
770 global.restoreSeeds(origSeeds);
772 delete debugMovement;
773 saveGameSession = false;
774 replayGameSession = false;
775 doGameSavingPlaying = Replay.None;
778 origStats.global = global;
779 level.stats = origStats;
785 global.config = origConfig;
791 // ////////////////////////////////////////////////////////////////////////// //
792 final bool saveGame (string gmname) {
793 return appSaveOptions(level, gmname);
797 final bool loadGame (string gmname) {
798 auto olddel = ImmediateDelete;
799 ImmediateDelete = false;
801 auto stats = level.stats;
804 auto lvl = appLoadOptions(GameLevel, gmname);
806 //lvl.global.config = config;
811 level.loserGPU = loserGPU;
812 global = level.global;
813 global.config = config;
815 level.sprStore = sprStore;
816 level.bgtileStore = bgtileStore;
819 level.onBeforeFrame = &beforeNewFrame;
820 level.onAfterFrame = &afterNewFrame;
821 level.onInterFrame = &interFrame;
822 level.onLevelExitedCB = &levelExited;
823 level.onCameraTeleported = &cameraTeleportedCB;
825 //level.viewWidth = Video.screenWidth;
826 //level.viewHeight = Video.screenHeight;
827 level.viewWidth = 320*3;
828 level.viewHeight = 240*3;
831 level.centerViewAtPlayer();
832 teleportCameraAt(level.viewStart);
834 recalcCameraCoords(0);
839 level.stats.global = level.global;
841 ImmediateDelete = olddel;
842 CollectGarbage(true); // destroy delayed objects too
847 // ////////////////////////////////////////////////////////////////////////// //
848 float lastThinkerTime;
849 int replaySkipFrame = 0;
852 final void onTimePasses () {
853 float curTime = GetTickCount();
854 if (lastThinkerTime > 0) {
855 if (curTime < lastThinkerTime) {
856 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
857 lastThinkerTime = curTime;
860 if (replayFastForward && replaySkipFrame) {
862 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
865 level.processThinkers(curTime-lastThinkerTime);
867 lastThinkerTime = curTime;
871 final void resetFramesAndForceOne () {
872 float curTime = GetTickCount();
873 lastThinkerTime = curTime;
875 auto wasPaused = level.gamePaused;
876 level.gamePaused = false;
877 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
878 level.processThinkers(GameLevel::FrameTime);
879 level.gamePaused = wasPaused;
880 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
884 // ////////////////////////////////////////////////////////////////////////// //
885 private float currFrameDelta; // so level renderer can properly interpolate the player
886 private GameLevel::IVec2D camPrev, camCurr;
887 private GameLevel::IVec2D camShake;
888 private GameLevel::IVec2D viewCameraPos;
891 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
896 viewCameraPos.x = pos.x;
897 viewCameraPos.y = pos.y;
903 // call `recalcCameraCoords()` to get real camera coords after this
904 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
905 // check if camera is moved too far, and teleport it
907 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
908 abs(camCurr.y-pos.y)/global.scale >= 16*4))
910 teleportCameraAt(pos);
912 camPrev.x = camCurr.x;
913 camPrev.y = camCurr.y;
917 camShake.x = level.shakeDir.x*global.scale;
918 camShake.y = level.shakeDir.y*global.scale;
922 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
923 currFrameDelta = frameDelta;
924 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
925 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
927 viewCameraPos.x += camShake.x;
928 viewCameraPos.y += camShake.y;
932 GameLevel::SavedKeyState savedKeyState;
934 final void pauseGame () {
935 if (!level.gamePaused) {
936 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
937 level.gamePaused = true;
938 global.pauseAllSounds();
943 final void unpauseGame () {
944 if (level.gamePaused) {
945 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
946 level.gamePaused = false;
947 level.gameShowHelp = false;
948 level.gameHelpScreen = 0;
949 //lastThinkerTime = 0;
950 global.resumeAllSounds();
952 pauseRequested = false;
953 helpRequested = false;
958 final void beforeNewFrame (bool frameSkip) {
961 level.disablePlayerThink = true;
964 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
965 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
966 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
968 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
969 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
970 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
971 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
973 level.disablePlayerThink = false;
979 if (!level.gamePaused) {
980 // save seeds for afterframe processing
982 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
983 debugMovement.otherSeed = global.globalOtherSeed;
984 debugMovement.roomSeed = global.globalRoomSeed;
988 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
990 #ifdef BIGGER_REPLAY_DATA
991 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
992 debugMovement.keypresses.length += 1;
993 level.keysSaveState(debugMovement.keypresses[$-1]);
994 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
995 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
999 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
1000 #ifdef BIGGER_REPLAY_DATA
1001 if (debugMovement.keypos < debugMovement.keypresses.length) {
1002 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1003 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1004 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1005 ++debugMovement.keypos;
1011 auto code = debugMovement.getKey(out kbidx, out down);
1012 if (code == DebugSessionMovement::END_OF_RECORD) {
1013 // do this in main loop, so we can view totals
1017 if (code == DebugSessionMovement::END_OF_FRAME) {
1020 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1021 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1029 final void afterNewFrame (bool frameSkip) {
1030 if (!replayFastForward) replaySkipFrame = 0;
1032 if (level.gamePaused) return;
1034 if (!level.gamePaused) {
1035 if (doGameSavingPlaying != Replay.None) {
1036 if (doGameSavingPlaying == Replay.Saving) {
1037 replayFastForward = false; // just in case
1038 #ifndef BIGGER_REPLAY_DATA
1039 debugMovement.addEndOfFrame();
1041 auto stt = GetTickCount();
1042 if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1043 } else if (doGameSavingPlaying == Replay.Replaying) {
1044 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1045 replaySkipFrame = 1;
1051 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1052 //SoundSystem.UpdateSounds();
1054 //if (!freeRide) level.fixCamera();
1055 setNewCameraPos(level.viewStart);
1057 prevCameraX = currCameraX;
1058 prevCameraY = currCameraY;
1059 currCameraX = level.cameraX;
1060 currCameraY = level.cameraY;
1061 // disable camera interpolation if the screen is shaking
1062 if (level.shakeX|level.shakeY) {
1063 prevCameraX = currCameraX;
1064 prevCameraY = currCameraY;
1067 // disable camera interpolation if it moves too far away
1068 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1069 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1071 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1073 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1074 pauseRequested = false;
1076 if (helpRequested) {
1077 helpRequested = false;
1078 level.gameShowHelp = true;
1079 level.gameHelpScreen = 0;
1082 if (!showHelp) showHelp = true;
1084 writeln("active objects in level: ", level.activeItemsCount);
1090 final void interFrame (float frameDelta) {
1091 if (!config.interpolateMovement) return;
1092 recalcCameraCoords(frameDelta);
1096 final void cameraTeleportedCB () {
1097 teleportCameraAt(level.viewStart);
1098 recalcCameraCoords(0);
1102 // ////////////////////////////////////////////////////////////////////////// //
1104 final void setColorByIdx (bool isset, int col) {
1106 // missed collision: red
1107 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1108 } else if (col == -999) {
1109 // superfluous collision: blue
1110 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1111 } else if (col <= 0) {
1112 // no collision: yellow
1113 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1114 } else if (col > 0) {
1116 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1121 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1123 CollisionMask cm = CollisionMask.Create(frm, false);
1125 int scale = global.config.scale;
1126 int bx0, by0, bx1, by1;
1127 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1128 Video.color = 0x7f_00_00_ff;
1129 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1130 if (!cm.isEmptyMask) {
1131 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1132 foreach (int iy; 0..cm.height) {
1133 foreach (int ix; 0..cm.width) {
1134 int v = cm.mask[ix, iy];
1135 foreach (int dx; 0..32) {
1138 Video.color = 0x3f_00_ff_00;
1139 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1148 foreach (int iy; 0..frm.tex.height) {
1149 foreach (int ix; 0..(frm.tex.width+31)/31) {
1150 foreach (int dx; 0..32) {
1152 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1153 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1154 setColorByIdx(true, col);
1155 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1157 Video.color = 0xaf_00_ff_00;
1159 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1165 if (frm.bw > 0 && frm.bh > 0) {
1166 setColorByIdx(true, col);
1167 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1168 Video.color = 0xff_00_00;
1169 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1178 // ////////////////////////////////////////////////////////////////////////// //
1179 transient int drawStats;
1180 transient array!int statsTopItem;
1183 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1184 auto sa = string(a.objName).toUpperCase;
1185 auto sb = string(b.objName).toUpperCase;
1190 final int getStatsTopItem () {
1191 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1195 final void setStatsTopItem (int val) {
1196 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1197 statsTopItem[drawStats] = val;
1201 final void resetStatsTopItem () {
1206 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1207 sprStore.loadFont('sFontSmall');
1213 final int calcStatsVisItems () {
1216 statsDrawGetStartPosLoadFont(currX, currY);
1217 int endY = level.viewHeight-(currY*2);
1218 return max(1, endY/sprStore.getFontHeight(scale));
1222 int getStatsItemCount () {
1223 switch (drawStats) {
1224 case 2: return level.stats.totalKills.length;
1225 case 3: return level.stats.totalDeaths.length;
1226 case 4: return level.stats.totalCollected.length;
1232 final void statsMoveUp () {
1233 int count = getStatsItemCount();
1234 if (count < 0) return;
1235 int visItems = calcStatsVisItems();
1236 if (count <= visItems) { resetStatsTopItem(); return; }
1237 int top = getStatsTopItem();
1239 setStatsTopItem(top-1);
1243 final void statsMoveDown () {
1244 int count = getStatsItemCount();
1245 if (count < 0) return;
1246 int visItems = calcStatsVisItems();
1247 if (count <= visItems) { resetStatsTopItem(); return; }
1248 int top = getStatsTopItem();
1249 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1250 top = clamp(top+1, 0, count-visItems);
1251 setStatsTopItem(top);
1255 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1256 arr.sort(&totalsNameCmpCB);
1260 statsDrawGetStartPosLoadFont(currX, currY);
1262 int endY = level.viewHeight-(currY*2);
1263 int visItems = calcStatsVisItems();
1265 if (arr.length <= visItems) resetStatsTopItem();
1267 int topItem = getStatsTopItem();
1271 Video.color = 0x3f_ff_ff_00;
1272 auto spr = sprStore['sPageUp'];
1273 spr.frames[0].blitAt(currX-28, currY, scale);
1276 // "downscroll" mark
1277 if (topItem+visItems < arr.length) {
1278 Video.color = 0x3f_ff_ff_00;
1279 auto spr = sprStore['sPageDown'];
1280 spr.frames[0].blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1283 Video.color = 0xff_ff_00;
1284 int hiColor = 0x00_ff_00;
1285 int hiColor1 = 0xf_ff_ff;
1288 while (it < arr.length && visItems-- > 0) {
1289 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);
1290 currY += sprStore.getFontHeight(scale);
1296 void drawStatsScreen () {
1297 int deathCount, killCount, collectCount;
1299 sprStore.loadFont('sFontSmall');
1301 Video.color = 0xff_ff_ff;
1302 level.drawTextAtS3Centered(240-2-8, "ESC-RETURN F10-QUIT CTRL+DEL-SUICIDE");
1303 level.drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
1305 Video.color = 0xff_ff_00;
1306 int hiColor = 0x00_ff_00;
1308 switch (drawStats) {
1309 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1310 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1311 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1314 if (drawStats > 1) {
1316 foreach (ref auto i; statsTopItem) i = 0;
1321 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1322 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1323 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1329 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1330 currY += sprStore.getFontHeight(scale);
1332 int gw = level.stats.gamesWon;
1333 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1334 currY += sprStore.getFontHeight(scale);
1336 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1337 currY += sprStore.getFontHeight(scale);
1339 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1340 currY += sprStore.getFontHeight(scale);
1342 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1343 currY += sprStore.getFontHeight(scale);
1345 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1346 currY += sprStore.getFontHeight(scale);
1348 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1349 currY += sprStore.getFontHeight(scale);
1351 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1352 currY += sprStore.getFontHeight(scale);
1354 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1355 currY += sprStore.getFontHeight(scale);
1357 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1358 currY += sprStore.getFontHeight(scale);
1360 int gs = level.stats.totalGhostSummoned;
1361 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1362 currY += sprStore.getFontHeight(scale);
1364 currY += sprStore.getFontHeight(scale);
1365 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1366 currY += sprStore.getFontHeight(scale);
1371 if (Video.frameTime == 0) {
1373 Video.requestRefresh();
1378 if (level.framesProcessedFromLastClear < 1) return;
1379 calcMouseMapCoords();
1381 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1382 Video.clearScreen();
1383 Video.stencil = false;
1384 Video.color = 0xff_ff_ff;
1385 Video.textureFiltering = false;
1386 // don't touch framebuffer alpha
1387 Video.colorMask = Video::CMask.Colors;
1389 Video::ScissorRect scsave;
1390 bool doRestoreGL = false;
1393 if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1395 Video.getScissor(scsave);
1396 Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1397 Video.glPushMatrix();
1398 Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1402 if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1404 float scx = float(Video.screenWidth)/float(level.viewWidth);
1405 float scy = float(Video.screenHeight)/float(level.viewHeight);
1406 float scale = fmin(scx, scy);
1407 int calcedW = trunc(level.viewWidth*scale);
1408 int calcedH = trunc(level.viewHeight*scale);
1409 Video.getScissor(scsave);
1410 int ofsx = (Video.screenWidth-calcedW)/2;
1411 int ofsy = (Video.screenHeight-calcedH)/2;
1412 Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1413 Video.glPushMatrix();
1414 Video.glTranslate(ofsx, ofsy);
1415 Video.glScale(scale, scale);
1418 //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1419 //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1423 level.viewOffsetX = 0;
1424 level.viewOffsetY = 0;
1425 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1428 float scx = float(Video.screenWidth)/float(level.viewWidth);
1429 float scy = float(Video.screenHeight)/float(level.viewHeight);
1430 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1436 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1439 if (level.gamePaused && showHelp != 2) {
1440 if (mouseLevelX != int.min) {
1441 int scale = level.global.scale;
1442 if (renderMouseRect) {
1443 Video.color = 0xcf_ff_ff_00;
1444 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1446 if (renderMouseTile) {
1447 Video.color = 0xaf_ff_00_00;
1448 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1453 switch (doGameSavingPlaying) {
1455 Video.color = 0x7f_00_ff_00;
1456 sprStore.loadFont('sFont');
1457 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1459 case Replay.Replaying:
1460 if (level.player && !level.player.dead) {
1461 Video.color = 0x7f_ff_00_00;
1462 sprStore.loadFont('sFont');
1463 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1464 int th = sprStore.getFontHeight(2);
1465 if (replayFastForward) {
1466 sprStore.loadFont('sFontSmall');
1467 string sstr = va("x%d", replayFastForwardSpeed+1);
1468 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1473 if (saveGameSession) {
1474 Video.color = 0x7f_ff_7f_00;
1475 sprStore.loadFont('sFont');
1476 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1482 if (level.player && level.player.dead && !showHelp) {
1484 Video.color = 0x8f_00_00_00;
1485 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1490 if (true /*level.inWinCutscene == 0*/) {
1491 Video.color = 0xff_ff_ff;
1492 sprStore.loadFont('sFontSmall');
1493 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1495 "PRESS $PAY TO RESTART GAME\n"~
1497 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1499 "TOTAL PLAYING TIME: |%s|"~
1501 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1502 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1503 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1505 GameLevel.time2str(level.stats.playingTime)
1507 kmsg = global.expandString(kmsg);
1508 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1515 Video.color = 0xff_7f_00;
1516 sprStore.loadFont('sFontSmall');
1517 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1518 auto spf = smask.frames[maskFrame];
1519 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1521 spf.bx, spf.by, spf.bw, spf.bh,
1522 (spf.maskEmpty ? "TAN" : "ONA"),
1523 (spf.precise ? "TAN" : "ONA")),
1526 //spf.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1527 //writeln("pos=(", maskSX, ",", maskSY, ")");
1528 int scale = global.config.scale;
1529 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1530 int mapX = xofs/scale+maskSX;
1531 int mapY = yofs/scale+maskSY;
1534 writeln("==== tiles ====");
1536 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1537 if (t.spectral || !t.isInstanceAlive) return false;
1538 Video.color = 0x7f_ff_00_00;
1539 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);
1540 auto tsf = t.getSpriteFrame();
1542 auto spf = smask.frames[maskFrame];
1543 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1544 int mapX = xofs/global.config.scale+maskSX;
1545 int mapY = yofs/global.config.scale+maskSY;
1548 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1549 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1550 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1554 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1555 Video.color = 0x7f_ff_00_00;
1556 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);
1560 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1562 Video.color = 0xaf_ff_ff_ff;
1563 spf.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1564 Video.color = 0xff_ff_00;
1565 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1569 int fx0, fy0, fx1, fy1;
1570 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1571 Video.color = 0x7f_00_00_ff;
1572 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1578 Video.color = 0x8f_00_00_00;
1579 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1581 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1586 Video.color = 0xff_ff_00;
1587 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1588 if (showHelp == 1) {
1589 int msx, msy, ww, wh;
1590 Video.getMousePos(out msx, out msy);
1591 Video.getRealWindowSize(out ww, out wh);
1592 if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1593 sprStore.loadFont('sFontSmall');
1594 Video.color = 0xff_ff_00;
1595 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1596 "F1: show this help\n"~
1598 "K : redefine keys\n"~
1599 "I : toggle interpolaion\n"~
1600 "N : create some blood\n"~
1601 "R : generate a new level\n"~
1602 "F : toggle \"Frozen Area\"\n"~
1603 "X : resurrect player\n"~
1604 "Q : teleport to exit\n"~
1605 "D : teleport to damel\n"~
1607 "C : cheat flags menu\n"~
1608 "P : cheat pickup menu\n"~
1609 "E : cheat enemy menu\n"~
1610 "Enter: cheat items menu\n"~
1612 "TAB: toggle 'freeroam' mode\n"~
1617 if (level) level.renderPauseOverlay();
1621 //SoundSystem.UpdateSounds();
1623 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1626 Video.setScissor(scsave);
1627 Video.glPopMatrix();
1632 Video.color = 0xaf_ff_ff_ff;
1633 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1638 // ////////////////////////////////////////////////////////////////////////// //
1639 transient bool gameJustOver;
1640 transient bool waitingForPayRestart;
1643 final void calcMouseMapCoords () {
1644 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1645 mouseLevelX = int.min;
1646 mouseLevelY = int.min;
1649 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1650 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1651 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1655 final void onEvent (ref event_t evt) {
1656 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1658 if (evt.type == ev_winfocus) {
1659 if (level && !evt.focused) {
1663 //writeln("FOCUS!");
1664 Video.getMousePos(out mouseX, out mouseY);
1669 if (evt.type == ev_mouse) {
1672 calcMouseMapCoords();
1675 if (evt.type == ev_keydown && evt.keycode == K_F12) {
1676 if (level) toggleFullscreen();
1680 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1681 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1682 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1685 if (evt.type == ev_keydown) {
1686 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1687 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1688 renderMouseTile = evt.bCtrl;
1689 renderMouseRect = evt.bAlt;
1692 if (evt.type == ev_keyup) {
1693 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1694 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1695 renderMouseTile = evt.bCtrl;
1696 renderMouseRect = evt.bAlt;
1699 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1700 int newScale = evt.keycode-48;
1701 if (global.config.scale != newScale) {
1702 global.config.scale = newScale;
1705 cameraTeleportedCB();
1712 if (evt.type == ev_mouse) {
1713 maskSX = evt.x/global.config.scale;
1714 maskSY = evt.y/global.config.scale;
1717 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1718 maskFrame = max(0, maskFrame-1);
1721 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1722 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1729 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1731 if (saveOptionsDG) saveOptionsDG();
1732 saveOptionsDG = none;
1734 //SoundSystem.UpdateSounds(); // just in case
1735 if (global.hasSpectacles) level.pickedSpectacles();
1738 optionsPane.onEvent(evt);
1742 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1743 if (evt.type == ev_keydown) {
1744 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1745 switch (evt.keycode) {
1746 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1747 case K_F2: if (showHelp != 2) unpauseGame(); return;
1748 case K_F10: Video.requestQuit(); return;
1749 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1753 allowRender = !allowRender;
1759 case K_UPARROW: case K_PAD8:
1760 if (drawStats) statsMoveUp();
1763 case K_DOWNARROW: case K_PAD2:
1764 if (drawStats) statsMoveDown();
1767 case K_LEFTARROW: case K_PAD4:
1768 if (level && showHelp == 2 && level.gameShowHelp) {
1769 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1773 case K_RIGHTARROW: case K_PAD6:
1774 if (level && showHelp == 2 && level.gameShowHelp) {
1775 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1789 resetFramesAndForceOne();
1795 if (/*evt.bCtrl &&*/ showHelp != 2) {
1805 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1806 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1807 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1808 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1809 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1810 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1811 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1812 //case K_j: global.hasJordans = !global.hasJordans; return;
1814 if (/*evt.bCtrl &&*/ showHelp != 2) {
1815 level.resurrectPlayer();
1820 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1821 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1822 if (evt.bAlt && level.player && level.player.dead) {
1823 saveGameSession = false;
1824 replayGameSession = true;
1828 if (/*evt.bCtrl &&*/ showHelp != 2) {
1829 if (evt.bShift) global.idol = false;
1830 level.generateLevel();
1831 level.centerViewAtPlayer();
1832 teleportCameraAt(level.viewStart);
1833 resetFramesAndForceOne();
1837 global.toggleMusic();
1840 if (/*evt.bCtrl &&*/ showHelp != 2) {
1841 foreach (MapTile t; level.allExits) {
1842 if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1843 level.teleportPlayerTo(t.ix+8, t.iy+8);
1851 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1852 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1854 level.teleportPlayerTo(damsel.ix, damsel.iy);
1860 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1864 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1867 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1870 level.teleportPlayerTo(obj.ix, obj.iy-4);
1876 if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1877 if (level && mouseLevelX != int.min) {
1878 int scale = level.global.scale;
1879 int mapX = mouseLevelX;
1880 int mapY = mouseLevelY;
1881 level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1887 if (evt.bCtrl && showHelp != 2) {
1888 if (level && mouseLevelX != int.min) {
1889 int scale = level.global.scale;
1890 int mapX = mouseLevelX;
1891 int mapY = mouseLevelY;
1892 level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1898 if (evt.bCtrl && showHelp != 2) {
1899 if (level && mouseLevelX != int.min) {
1900 int scale = level.global.scale;
1901 int mapX = mouseLevelX;
1902 int mapY = mouseLevelY;
1903 level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1904 level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1910 if (evt.bCtrl && showHelp != 2) {
1911 if (level && mouseLevelX != int.min) {
1912 int scale = level.global.scale;
1913 int mapX = mouseLevelX;
1914 int mapY = mouseLevelY;
1915 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1919 if (evt.bAlt && showHelp != 2) {
1920 if (level && mouseLevelX != int.min) {
1921 int scale = level.global.scale;
1922 int mapX = mouseLevelX;
1923 int mapY = mouseLevelY;
1924 level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1930 if (level && mouseLevelX != int.min) {
1931 int scale = level.global.scale;
1932 int mapX = mouseLevelX;
1933 int mapY = mouseLevelY;
1936 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1937 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1938 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1942 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1943 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1949 if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1950 auto obj = level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1954 case K_DELETE: // suicide
1955 if (doGameSavingPlaying == Replay.None) {
1956 if (level.player && !level.player.dead && evt.bCtrl) {
1957 global.hasAnkh = false;
1958 level.global.plife = 1;
1959 level.player.invincible = 0;
1960 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1961 if (xplo) xplo.suicide = true;
1968 if (level.player && !level.player.dead && evt.bAlt) {
1969 if (doGameSavingPlaying != Replay.None) {
1970 if (doGameSavingPlaying == Replay.Replaying) {
1972 } else if (doGameSavingPlaying == Replay.Saving) {
1973 saveGameMovement(dbgSessionMovementFileName, packit:true);
1975 doGameSavingPlaying = Replay.None;
1977 saveGameSession = false;
1978 replayGameSession = false;
1985 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1986 level.stats.setMoneyCheat();
1987 level.stats.addMoney(10000);
1993 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1994 if (level.player && level.player.dead) {
1995 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1998 pauseRequested = true;
2003 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2004 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2005 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2008 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2011 if (!level.player || !level.player.dead) {
2012 gameJustOver = false;
2013 } else if (level.player && level.player.dead) {
2014 if (!gameJustOver) {
2016 gameJustOver = true;
2017 waitingForPayRestart = true;
2018 level.clearKeysPressRelease();
2019 if (doGameSavingPlaying == Replay.None) {
2020 stopReplaying(); // just in case
2024 replayFastForward = false;
2025 if (doGameSavingPlaying == Replay.Saving) {
2026 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2027 doGameSavingPlaying = Replay.None;
2028 //clearGameMovement();
2029 saveGameSession = false;
2030 replayGameSession = false;
2033 if (evt.type == ev_keydown || evt.type == ev_keyup) {
2034 bool down = (evt.type == ev_keydown);
2035 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2036 if (down && evt.keycode == K_f) {
2038 if (replayFastForwardSpeed != 4) {
2039 replayFastForwardSpeed = 4;
2040 replayFastForward = true;
2042 replayFastForward = !replayFastForward;
2045 replayFastForwardSpeed = 2;
2046 replayFastForward = !replayFastForward;
2050 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2051 foreach (int kbidx, int kval; global.config.keybinds) {
2052 if (kval && kval == evt.keycode) {
2053 #ifndef BIGGER_REPLAY_DATA
2054 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2056 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2060 if (level.player && level.player.dead) {
2061 if (down && evt.keycode == K_r && evt.bAlt) {
2062 saveGameSession = false;
2063 replayGameSession = true;
2066 if (down && evt.keycode == K_s && evt.bAlt) {
2067 bool wasSaveReq = saveGameSession;
2068 stopReplaying(); // just in case
2069 saveGameSession = !wasSaveReq;
2070 replayGameSession = false;
2073 if (replayGameSession) {
2074 stopReplaying(); // just in case
2075 saveGameSession = false;
2076 replayGameSession = false;
2077 loadGameMovement(dbgSessionMovementFileName);
2078 loadGame(dbgSessionStateFileName);
2079 doGameSavingPlaying = Replay.Replaying;
2082 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2083 if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2084 if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2085 if (waitingForPayRestart) {
2086 level.isKeyReleased(GameConfig::Key.Pay);
2087 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2089 level.isKeyPressed(GameConfig::Key.Pay);
2090 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2091 auto doSave = saveGameSession;
2092 stopReplaying(); // just in case
2093 level.clearKeysPressRelease();
2094 level.restartGame();
2095 level.generateNormalLevel();
2097 saveGameSession = false;
2098 replayGameSession = false;
2099 writeln("DBG: saving game session...");
2100 clearGameMovement();
2101 doGameSavingPlaying = Replay.Saving;
2102 saveGame(dbgSessionStateFileName);
2103 //saveGameMovement(dbgSessionMovementFileName);
2114 void levelExited () {
2120 void closeVideo () {
2121 if (fullscreen && Video.isInitialized) Video.showMouseCursor();
2122 Video.closeScreen();
2126 void initializeVideo () {
2127 Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2128 if (Video.realStencilBits < 8) {
2129 Video.closeScreen();
2130 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2133 if (!loserGPU && !Video.framebufferHasAlpha) {
2134 Video.closeScreen();
2135 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!\nRun the game with \"--loser-gpu\" arg if you still want to play.");
2138 if (!Video.framebufferHasAlpha) {
2140 if (level) level.loserGPU = true;
2143 if (!Video.glHasNPOT) {
2144 Video.closeScreen();
2145 FatalError("=== YOUR GPU SUX! ===\nno NPOT texture support!");
2148 if (fullscreen) Video.hideMouseCursor();
2152 void toggleFullscreen () {
2154 fullscreen = !fullscreen;
2159 final void runGameLoop () {
2160 Video.frameTime = 0; // unlimited FPS
2161 lastThinkerTime = 0;
2163 sprStore = SpawnObject(SpriteStore);
2164 sprStore.bDumpLoaded = false;
2166 bgtileStore = SpawnObject(BackTileStore);
2167 bgtileStore.bDumpLoaded = false;
2169 level = SpawnObject(GameLevel);
2170 level.loserGPU = loserGPU;
2171 level.setup(global, sprStore, bgtileStore);
2173 level.BuildYear = BuildYear;
2174 level.BuildMonth = BuildMonth;
2175 level.BuildDay = BuildDay;
2176 level.BuildHour = BuildHour;
2177 level.BuildMin = BuildMin;
2179 level.global = global;
2180 level.sprStore = sprStore;
2181 level.bgtileStore = bgtileStore;
2184 //level.stats.introViewed = 0;
2186 if (level.stats.introViewed == 0) {
2187 startMode = StartMode.Intro;
2188 writeln("FORCED INTRO");
2190 //writeln("INTRO VIWED: ", level.stats.introViewed);
2191 if (level.global.config.skipIntro) startMode = StartMode.Title;
2194 level.onBeforeFrame = &beforeNewFrame;
2195 level.onAfterFrame = &afterNewFrame;
2196 level.onInterFrame = &interFrame;
2197 level.onLevelExitedCB = &levelExited;
2198 level.onCameraTeleported = &cameraTeleportedCB;
2201 maskSX = -0x0ff_fff;
2203 smask = sprStore['sExplosionMask'];
2207 level.viewWidth = 320*3;
2208 level.viewHeight = 240*3;
2210 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2211 //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2212 fullscreen = global.config.startFullscreen;
2215 sprStore.loadFont('sFontSmall');
2217 //SoundSystem.SwapStereo = config.swapStereo;
2218 SoundSystem.NumChannels = 32;
2219 SoundSystem.MaxHearingDistance = 12000;
2220 //SoundSystem.DopplerFactor = 1.0f;
2221 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2222 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2223 SoundSystem.ReferenceDistance = 16.0f*4;
2224 SoundSystem.MaxDistance = 16.0f*(5*10);
2226 SoundSystem.Initialize();
2227 if (!SoundSystem.IsInitialized) {
2228 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2229 global.soundDisabled = true;
2230 global.musicDisabled = true;
2232 global.fixVolumes();
2234 level.restartGame(); // this will NOT generate a new level
2239 texTigerEye = GLTexture.Load("teye0.png");
2241 if (global.cheatEndGameSequence) {
2242 level.winTime = 12*60+42;
2243 level.stats.money = 6666;
2244 switch (global.cheatEndGameSequence) {
2245 case 1: default: level.startWinCutscene(); break;
2246 case 2: level.startWinCutsceneVolcano(); break;
2247 case 3: level.startWinCutsceneWinFall(); break;
2250 switch (startMode) {
2251 case StartMode.Title: level.restartTitle(); break;
2252 case StartMode.Intro: level.restartIntro(); break;
2253 case StartMode.Stars: level.restartStarsRoom(); break;
2254 case StartMode.Sun: level.restartSunRoom(); break;
2255 case StartMode.Moon: level.restartMoonRoom(); break;
2257 level.generateNormalLevel();
2258 if (startMode == StartMode.Dead) {
2259 level.player.dead = true;
2260 level.player.visible = false;
2266 //global.rope = 666;
2267 //global.bombs = 666;
2269 //global.globalRoomSeed = 871520037;
2270 //global.globalOtherSeed = 1047036290;
2272 //level.createTitleRoom();
2273 //level.createTrans4Room();
2274 //level.createOlmecRoom();
2275 //level.generateLevel();
2277 //level.centerViewAtPlayer();
2278 teleportCameraAt(level.viewStart);
2279 //writeln(Video.swapInterval);
2281 Video.runEventLoop();
2283 SoundSystem.Shutdown();
2285 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2293 // ////////////////////////////////////////////////////////////////////////// //
2294 // duplicates are not allowed!
2295 final void checkGameObjNames () {
2296 array!(class!Object) known;
2298 int classCount = 0, namedCount = 0;
2299 foreach AllClasses(Object, out cc) {
2300 auto gn = GetClassGameObjName(cc);
2302 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2303 auto nid = NameToInt(gn);
2304 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));
2310 writeln(classCount, " classes, ", namedCount, " game object classes.");
2314 // ////////////////////////////////////////////////////////////////////////// //
2315 #include "timelimit.vc"
2316 //const int TimeLimitDate = 2018232;
2319 void performTimeCheck () {
2320 #ifdef DISABLE_TIME_CHECK
2322 if (TigerEye) return;
2325 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2328 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2330 int tldate = tm.year*1000+tm.yday;
2332 if (tldate > TimeLimitDate) {
2333 level.maxPlayingTime = 24;
2335 //writeln("*** days left: ", TimeLimitDate-tldate);
2341 void setupCheats () {
2344 //level.stats.resetTunnelPrices();
2345 startMode = StartMode.Alive;
2346 global.currLevel = 10;
2347 //global.scumGenAlienCraft = true;
2348 //global.scumGenYetiLair = true;
2351 startMode = StartMode.Alive;
2352 global.currLevel = 8;
2354 level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2355 level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2356 level.stats.tunnel1Active = false;
2357 level.stats.tunnel2Active = false;
2358 level.stats.tunnel3Active = false;
2362 startMode = StartMode.Alive;
2363 global.currLevel = 2;
2364 global.scumGenShop = true;
2365 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2366 //global.config.scale = 1;
2369 startMode = StartMode.Alive;
2370 global.currLevel = 13;
2371 global.config.scale = 2;
2374 startMode = StartMode.Alive;
2375 global.currLevel = 13;
2376 global.config.scale = 1;
2377 global.cityOfGold = true;
2380 startMode = StartMode.Alive;
2381 global.currLevel = 5;
2382 global.genBlackMarket = true;
2385 startMode = StartMode.Alive;
2386 global.currLevel = 2;
2387 global.scumGenShop = true;
2388 global.scumGenShopType = GameGlobal::ShopType.Weapon;
2389 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2390 //global.config.scale = 1;
2393 //startMode = StartMode.Intro;
2396 global.currLevel = 2;
2397 startMode = StartMode.Alive;
2400 global.currLevel = 5;
2401 startMode = StartMode.Alive;
2402 global.scumGenLake = true;
2403 global.config.scale = 1;
2406 startMode = StartMode.Alive;
2407 global.cheatCanSkipOlmec = true;
2408 global.currLevel = 16;
2409 //global.currLevel = 5;
2410 //global.currLevel = 13;
2411 //global.config.scale = 1;
2413 //startMode = StartMode.Dead;
2414 //startMode = StartMode.Title;
2415 //startMode = StartMode.Stars;
2416 //startMode = StartMode.Sun;
2417 startMode = StartMode.Moon;
2419 //global.scumGenSacrificePit = true;
2420 //global.scumAlwaysSacrificeAltar = true;
2422 // first lush jungle level
2423 //global.levelType = 1;
2425 global.scumGenCemetary = true;
2427 //global.idol = false;
2428 //global.currLevel = 5;
2430 //global.isTunnelMan = true;
2433 //global.currLevel = 5;
2434 //global.scumGenLake = true;
2436 //global.currLevel = 5;
2437 //global.currLevel = 9;
2438 //global.currLevel = 13;
2439 //global.currLevel = 14;
2440 //global.cheatEndGameSequence = 1;
2443 //global.currLevel = 6;
2444 global.scumGenAlienCraft = true;
2445 global.currLevel = 9;
2446 //global.scumGenYetiLair = true;
2447 //global.genBlackMarket = true;
2448 //startDead = false;
2449 startMode = StartMode.Alive;
2452 global.cheatCanSkipOlmec = true;
2453 global.currLevel = 15;
2454 startMode = StartMode.Alive;
2457 global.scumGenShop = true;
2458 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2459 global.scumGenShopType = GameGlobal::ShopType.Craps;
2460 //global.scumGenShopType = 6; // craps
2461 //global.scumGenShopType = 7; // kissing
2463 //global.scumAlwaysSacrificeAltar = true;
2467 void setupSeeds () {
2471 // ////////////////////////////////////////////////////////////////////////// //
2472 void main (ref array!string args) {
2473 foreach (string s; args) {
2474 if (s == "--loser-gpu") loserGPU = 1;
2477 checkGameObjNames();
2479 appSetName("k8spelunky");
2480 config = SpawnObject(GameConfig);
2481 global = SpawnObject(GameGlobal);
2482 global.config = config;
2483 config.heroType = GameConfig::Hero.Spelunker;
2485 global.randomizeSeedAll();
2487 fillCheatPickupList();
2488 fillCheatItemsList();
2489 fillCheatEnemiesList();
2492 loadKeyboardBindings();