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 **********************************************************************************/
26 //#define QUIT_DOUBLE_ESC
30 //#define BIGGER_REPLAY_DATA
32 // ////////////////////////////////////////////////////////////////////////// //
33 #include "mapent/0all.vc"
34 #include "PlayerPawn.vc"
35 #include "PlayerPowerup.vc"
36 #include "GameLevel.vc"
39 // ////////////////////////////////////////////////////////////////////////// //
40 #include "uisimple.vc"
43 // ////////////////////////////////////////////////////////////////////////// //
44 class DebugSessionMovement : Object;
46 #ifdef BIGGER_REPLAY_DATA
47 array!(GameLevel::SavedKeyState) keypresses;
49 array!ubyte keypresses; // on each frame
51 GameConfig playconfig;
54 transient int otherSeed, roomSeed;
57 override void Destroy () {
59 keypresses.length = 0;
64 final void resetReplay () {
69 #ifndef BIGGER_REPLAY_DATA
70 final void addKey (int kbidx, bool down) {
71 if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
72 keypresses[$] = kbidx|(down ? 0x80 : 0);
76 final void addEndOfFrame () {
87 final int getKey (out int kbidx, out bool down) {
88 if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
89 if (keypos >= keypresses.length) return END_OF_RECORD;
90 ubyte b = keypresses[keypos++];
91 if (b == 0xff) return END_OF_FRAME;
99 // ////////////////////////////////////////////////////////////////////////// //
100 class TempOptionsKeys : Object;
102 int[16*GameConfig::MaxActionBinds] keybinds;
106 // ////////////////////////////////////////////////////////////////////////// //
109 transient string dbgSessionStateFileName = "debug_game_session_state";
110 transient string dbgSessionMovementFileName = "debug_game_session_movement";
111 const float dbgSessionSaveIntervalInSeconds = 30;
113 GLTexture texTigerEye;
117 SpriteStore sprStore;
118 BackTileStore bgtileStore;
121 int mouseX = int.min, mouseY = int.min;
122 int mouseLevelX = int.min, mouseLevelY = int.min;
123 bool renderMouseTile;
124 bool renderMouseRect;
136 StartMode startMode = StartMode.Intro;
140 bool replayFastForward = false;
141 int replayFastForwardSpeed = 2;
142 bool saveGameSession = false;
143 bool replayGameSession = false;
149 Replay doGameSavingPlaying = Replay.None;
150 float saveMovementLastTime = 0;
151 DebugSessionMovement debugMovement;
152 GameStats origStats; // for replaying
153 GameConfig origConfig; // for replaying
154 GameGlobal::SavedSeeds origSeeds;
162 transient int maskSX, maskSY;
163 transient SpriteImage smask;
164 transient int maskFrame;
168 // ////////////////////////////////////////////////////////////////////////// //
169 final void saveKeyboardBindings () {
170 auto tok = SpawnObject(TempOptionsKeys);
171 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
172 appSaveOptions(tok, "keybindings");
177 final void loadKeyboardBindings () {
178 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
180 if (tok.kbversion != TempOptionsKeys.default.kbversion) {
181 global.config.resetKeybindings();
183 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
190 // ////////////////////////////////////////////////////////////////////////// //
191 void saveGameOptions () {
192 appSaveOptions(global.config, "config");
196 void loadGameOptions () {
197 auto cfg = appLoadOptions(GameConfig, "config");
199 auto oldHero = config.heroType;
200 auto tok = SpawnObject(TempOptionsKeys);
201 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
202 delete global.config;
205 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
207 writeln("config loaded");
208 global.restartMusic();
210 //config.heroType = GameConfig::Hero.Spelunker;
211 config.heroType = oldHero;
214 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
218 // ////////////////////////////////////////////////////////////////////////// //
219 void saveGameStats () {
220 if (level.stats) appSaveOptions(level.stats, "stats");
224 void loadGameStats () {
225 auto stats = appLoadOptions(GameStats, "stats");
230 if (!level.stats) level.stats = SpawnObject(GameStats);
231 level.stats.global = global;
235 // ////////////////////////////////////////////////////////////////////////// //
236 struct UIPaneSaveInfo {
238 UIPane::SaveInfo nfo;
241 transient UIPane optionsPane; // either options, or binding editor
243 transient GameLevel::IVec2D optionsPaneOfs;
244 transient void delegate () saveOptionsDG;
246 transient array!UIPaneSaveInfo optionsPaneState;
249 final void saveCurrentPane () {
250 if (!optionsPane || !optionsPane.id) return;
253 if (optionsPane.id == 'CheatFlags') {
254 if (instantGhost && level.ghostTimeLeft > 0) {
255 level.ghostTimeLeft = 1;
259 foreach (ref auto psv; optionsPaneState) {
260 if (psv.id == optionsPane.id) {
261 optionsPane.saveState(psv.nfo);
266 optionsPaneState.length += 1;
267 optionsPaneState[$-1].id = optionsPane.id;
268 optionsPane.saveState(optionsPaneState[$-1].nfo);
272 final void restoreCurrentPane () {
273 if (optionsPane) optionsPane.setupHotkeys(); // why not?
274 if (!optionsPane || !optionsPane.id) return;
275 foreach (ref auto psv; optionsPaneState) {
276 if (psv.id == optionsPane.id) {
277 optionsPane.restoreState(psv.nfo);
284 // ////////////////////////////////////////////////////////////////////////// //
285 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
286 if (!it.tagClass) return;
287 if (class!MapObject(it.tagClass)) {
288 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
289 it.owner.closeMe = true;
294 // ////////////////////////////////////////////////////////////////////////// //
295 transient array!(class!MapObject) cheatItemsList;
298 final void fillCheatItemsList () {
299 cheatItemsList.length = 0;
300 cheatItemsList[$] = ItemProjectileArrow;
301 cheatItemsList[$] = ItemWeaponShotgun;
302 cheatItemsList[$] = ItemWeaponAshShotgun;
303 cheatItemsList[$] = ItemWeaponPistol;
304 cheatItemsList[$] = ItemWeaponMattock;
305 cheatItemsList[$] = ItemWeaponMachete;
306 cheatItemsList[$] = ItemWeaponWebCannon;
307 cheatItemsList[$] = ItemWeaponSceptre;
308 cheatItemsList[$] = ItemWeaponBow;
309 cheatItemsList[$] = ItemBones;
310 cheatItemsList[$] = ItemFakeBones;
311 cheatItemsList[$] = ItemFishBone;
312 cheatItemsList[$] = ItemRock;
313 cheatItemsList[$] = ItemJar;
314 cheatItemsList[$] = ItemSkull;
315 cheatItemsList[$] = ItemGoldenKey;
316 cheatItemsList[$] = ItemGoldIdol;
317 cheatItemsList[$] = ItemCrystalSkull;
318 cheatItemsList[$] = ItemShellSingle;
319 cheatItemsList[$] = ItemChest;
320 cheatItemsList[$] = ItemCrate;
321 cheatItemsList[$] = ItemLockedChest;
322 cheatItemsList[$] = ItemDice;
323 cheatItemsList[$] = ItemBasketBall;
327 final UIPane createCheatItemsPane () {
328 if (!level.player) return none;
330 UIPane pane = SpawnObject(UIPane);
332 pane.sprStore = sprStore;
334 pane.width = 320*3-64;
335 pane.height = 240*3-64;
337 foreach (auto ipk; cheatItemsList) {
338 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
342 //optionsPaneOfs.x = 100;
343 //optionsPaneOfs.y = 50;
349 // ////////////////////////////////////////////////////////////////////////// //
350 transient array!(class!MapObject) cheatEnemiesList;
353 final void fillCheatEnemiesList () {
354 cheatEnemiesList.length = 0;
355 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
356 cheatEnemiesList[$] = EnemyBat;
357 cheatEnemiesList[$] = EnemySpiderHang;
358 cheatEnemiesList[$] = EnemySpider;
359 cheatEnemiesList[$] = EnemySnake;
360 cheatEnemiesList[$] = EnemyCaveman;
361 cheatEnemiesList[$] = EnemySkeleton;
362 cheatEnemiesList[$] = MonsterShopkeeper;
363 cheatEnemiesList[$] = EnemyZombie;
364 cheatEnemiesList[$] = EnemyVampire;
365 cheatEnemiesList[$] = EnemyFrog;
366 cheatEnemiesList[$] = EnemyGreenFrog;
367 cheatEnemiesList[$] = EnemyFireFrog;
368 cheatEnemiesList[$] = EnemyMantrap;
369 cheatEnemiesList[$] = EnemyScarab;
370 cheatEnemiesList[$] = EnemyFloater;
371 cheatEnemiesList[$] = EnemyBlob;
372 cheatEnemiesList[$] = EnemyMonkey;
373 cheatEnemiesList[$] = EnemyGoldMonkey;
374 cheatEnemiesList[$] = EnemyAlien;
375 cheatEnemiesList[$] = EnemyYeti;
376 cheatEnemiesList[$] = EnemyHawkman;
377 cheatEnemiesList[$] = EnemyUFO;
378 cheatEnemiesList[$] = EnemyYetiKing;
382 final UIPane createCheatEnemiesPane () {
383 if (!level.player) return none;
385 UIPane pane = SpawnObject(UIPane);
387 pane.sprStore = sprStore;
389 pane.width = 320*3-64;
390 pane.height = 240*3-64;
392 foreach (auto ipk; cheatEnemiesList) {
393 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
397 //optionsPaneOfs.x = 100;
398 //optionsPaneOfs.y = 50;
404 // ////////////////////////////////////////////////////////////////////////// //
405 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
408 final void fillCheatPickupList () {
409 cheatPickupList.length = 0;
410 cheatPickupList[$] = ItemPickupBombBag;
411 cheatPickupList[$] = ItemPickupBombBox;
412 cheatPickupList[$] = ItemPickupPaste;
413 cheatPickupList[$] = ItemPickupRopePile;
414 cheatPickupList[$] = ItemPickupShellBox;
415 cheatPickupList[$] = ItemPickupAnkh;
416 cheatPickupList[$] = ItemPickupCape;
417 cheatPickupList[$] = ItemPickupJetpack;
418 cheatPickupList[$] = ItemPickupUdjatEye;
419 cheatPickupList[$] = ItemPickupCrown;
420 cheatPickupList[$] = ItemPickupKapala;
421 cheatPickupList[$] = ItemPickupParachute;
422 cheatPickupList[$] = ItemPickupCompass;
423 cheatPickupList[$] = ItemPickupSpectacles;
424 cheatPickupList[$] = ItemPickupGloves;
425 cheatPickupList[$] = ItemPickupMitt;
426 cheatPickupList[$] = ItemPickupJordans;
427 cheatPickupList[$] = ItemPickupSpringShoes;
428 cheatPickupList[$] = ItemPickupSpikeShoes;
429 cheatPickupList[$] = ItemPickupTeleporter;
433 final UIPane createCheatPickupsPane () {
434 if (!level.player) return none;
436 UIPane pane = SpawnObject(UIPane);
438 pane.sprStore = sprStore;
440 pane.width = 320*3-64;
441 pane.height = 240*3-64;
443 foreach (auto ipk; cheatPickupList) {
444 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
448 //optionsPaneOfs.x = 100;
449 //optionsPaneOfs.y = 50;
455 // ////////////////////////////////////////////////////////////////////////// //
456 transient int instantGhost;
458 final UIPane createCheatFlagsPane () {
459 UIPane pane = SpawnObject(UIPane);
460 pane.id = 'CheatFlags';
461 pane.sprStore = sprStore;
463 pane.width = 320*3-64;
464 pane.height = 240*3-64;
468 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
469 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
470 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
471 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
472 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
473 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
474 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
475 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
476 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
477 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
478 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
479 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
480 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
481 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
482 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
483 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
484 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
485 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
487 optionsPaneOfs.x = 100;
488 optionsPaneOfs.y = 50;
494 final UIPane createOptionsPane () {
495 UIPane pane = SpawnObject(UIPane);
497 pane.sprStore = sprStore;
499 pane.width = 320*3-64;
500 pane.height = 240*3-64;
504 //!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.");
507 UILabel.Create(pane, "VISUAL OPTIONS");
508 UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
509 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
510 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).");
511 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
512 auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
513 startfs.onValueChanged = delegate void (int newval) {
514 Video.showMouseCursor();
519 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).");
520 fsmode.names[$] = "REAL";
521 fsmode.names[$] = "SCALED";
522 fsmode.onValueChanged = delegate void (int newval) {
524 Video.showMouseCursor();
531 UILabel.Create(pane, "");
532 UILabel.Create(pane, "HUD OPTIONS");
533 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
534 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.");
535 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
538 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
542 UILabel.Create(pane, "");
543 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
544 //!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.");
545 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
546 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
547 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.");
548 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.");
549 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
550 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.");
551 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
554 UILabel.Create(pane, "");
555 UILabel.Create(pane, "GAMEPLAY OPTIONS");
556 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.");
557 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
558 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
559 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!");
560 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.");
561 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.");
562 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
563 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.");
564 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.");
565 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.");
566 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.");
567 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?");
568 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.");
569 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.");
570 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.");
571 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
572 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
573 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
574 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
575 rstl.names[$] = "RANDOM";
576 rstl.names[$] = "NORMAL";
577 rstl.names[$] = "BIZARRE";
580 UILabel.Create(pane, "");
581 UILabel.Create(pane, "WHIP OPTIONS");
582 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
583 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
584 whiptype.names[$] = "NORMAL";
585 whiptype.names[$] = "LONG";
586 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.");
589 UILabel.Create(pane, "");
590 UILabel.Create(pane, "PLAYER OPTIONS");
591 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
592 herotype.names[$] = "SPELUNKY GUY";
593 herotype.names[$] = "DAMSEL";
594 herotype.names[$] = "TUNNEL MAN";
597 UILabel.Create(pane, "");
598 UILabel.Create(pane, "CHEAT OPTIONS");
599 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.");
600 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
601 plrlit.names[$] = "NEVER";
602 plrlit.names[$] = "FORCED DARKNESS";
603 plrlit.names[$] = "ALWAYS";
604 UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
605 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'.");
606 rdark.names[$] = "NEVER";
607 rdark.names[$] = "DEFAULT";
608 rdark.names[$] = "ALWAYS";
609 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.");
611 rghost.getNameCB = delegate string (int val) {
612 if (val < 0) return "INSTANT";
613 if (val == 0) return "NEVER";
614 if (val < 120) return va("%d SEC", val);
615 if (val%60 == 0) return va("%d MIN", val/60);
616 if (val%60 == 30) return va("%d.5 MIN", val/60);
617 return va("%d MIN, %d SEC", val/60, val%60);
619 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.");
621 UILabel.Create(pane, "");
622 UILabel.Create(pane, "CHEAT START OPTIONS");
623 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.");
624 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
625 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
626 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
627 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
630 UILabel.Create(pane, "");
631 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
632 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
633 mm.names[$] = "SILENCE";
634 mm.names[$] = "RESTART";
635 mm.names[$] = "DON'T TOUCH";
637 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
638 //mm.names[$] = "SILENCE";
639 mm.names[$] = "RESTART";
640 mm.names[$] = "DON'T TOUCH";
643 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
645 swstereo.onValueChanged = delegate void (int newval) {
646 SoundSystem.SwapStereo = newval;
650 UILabel.Create(pane, "");
651 UILabel.Create(pane, "SOUND CONTROL CENTER");
652 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
653 rmusonoff.onValueChanged = delegate void (int newval) {
654 global.restartMusic();
657 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
659 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
660 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
662 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
663 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
666 saveOptionsDG = delegate void () {
667 writeln("saving options");
670 optionsPaneOfs.x = 42;
671 optionsPaneOfs.y = 0;
677 final void createBindingsControl (UIPane pane, int keyidx) {
680 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
681 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
682 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
683 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
684 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
685 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
686 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
687 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
688 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
689 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
690 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
693 int arridx = GameConfig.getKeyIndex(keyidx);
694 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
698 final UIPane createBindingsPane () {
699 UIPane pane = SpawnObject(UIPane);
700 pane.id = 'KeyBindings';
701 pane.sprStore = sprStore;
703 pane.width = 320*3-64;
704 pane.height = 240*3-64;
706 createBindingsControl(pane, GameConfig::Key.Left);
707 createBindingsControl(pane, GameConfig::Key.Right);
708 createBindingsControl(pane, GameConfig::Key.Up);
709 createBindingsControl(pane, GameConfig::Key.Down);
710 createBindingsControl(pane, GameConfig::Key.Jump);
711 createBindingsControl(pane, GameConfig::Key.Run);
712 createBindingsControl(pane, GameConfig::Key.Attack);
713 createBindingsControl(pane, GameConfig::Key.Switch);
714 createBindingsControl(pane, GameConfig::Key.Pay);
715 createBindingsControl(pane, GameConfig::Key.Bomb);
716 createBindingsControl(pane, GameConfig::Key.Rope);
718 saveOptionsDG = delegate void () {
719 writeln("saving keys");
720 saveKeyboardBindings();
722 optionsPaneOfs.x = 120;
723 optionsPaneOfs.y = 140;
729 // ////////////////////////////////////////////////////////////////////////// //
730 void clearGameMovement () {
731 debugMovement = SpawnObject(DebugSessionMovement);
732 debugMovement.playconfig = SpawnObject(GameConfig);
733 debugMovement.playconfig.copyGameplayConfigFrom(config);
734 debugMovement.resetReplay();
738 void saveGameMovement (string fname, optional bool packit) {
739 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
740 saveMovementLastTime = GetTickCount();
744 void loadGameMovement (string fname) {
745 delete debugMovement;
746 debugMovement = appLoadOptions(DebugSessionMovement, fname);
747 debugMovement.resetReplay();
750 origStats = level.stats;
751 origStats.global = none;
752 level.stats = SpawnObject(GameStats);
753 level.stats.global = global;
756 config = debugMovement.playconfig;
757 global.config = config;
758 global.saveSeeds(origSeeds);
763 void stopReplaying () {
765 global.restoreSeeds(origSeeds);
767 delete debugMovement;
768 saveGameSession = false;
769 replayGameSession = false;
770 doGameSavingPlaying = Replay.None;
773 origStats.global = global;
774 level.stats = origStats;
780 global.config = origConfig;
786 // ////////////////////////////////////////////////////////////////////////// //
787 final bool saveGame (string gmname) {
788 return appSaveOptions(level, gmname);
792 final bool loadGame (string gmname) {
793 auto olddel = ImmediateDelete;
794 ImmediateDelete = false;
796 auto stats = level.stats;
799 auto lvl = appLoadOptions(GameLevel, gmname);
801 //lvl.global.config = config;
806 global = level.global;
807 global.config = config;
809 level.sprStore = sprStore;
810 level.bgtileStore = bgtileStore;
813 level.onBeforeFrame = &beforeNewFrame;
814 level.onAfterFrame = &afterNewFrame;
815 level.onInterFrame = &interFrame;
816 level.onLevelExitedCB = &levelExited;
817 level.onCameraTeleported = &cameraTeleportedCB;
819 //level.viewWidth = Video.screenWidth;
820 //level.viewHeight = Video.screenHeight;
821 level.viewWidth = 320*3;
822 level.viewHeight = 240*3;
825 level.centerViewAtPlayer();
826 teleportCameraAt(level.viewStart);
828 recalcCameraCoords(0);
833 level.stats.global = level.global;
835 ImmediateDelete = olddel;
836 CollectGarbage(true); // destroy delayed objects too
841 // ////////////////////////////////////////////////////////////////////////// //
842 float lastThinkerTime;
843 int replaySkipFrame = 0;
846 final void onTimePasses () {
847 float curTime = GetTickCount();
848 if (lastThinkerTime > 0) {
849 if (curTime < lastThinkerTime) {
850 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
851 lastThinkerTime = curTime;
854 if (replayFastForward && replaySkipFrame) {
856 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
859 level.processThinkers(curTime-lastThinkerTime);
861 lastThinkerTime = curTime;
865 final void resetFramesAndForceOne () {
866 float curTime = GetTickCount();
867 lastThinkerTime = curTime;
869 auto wasPaused = level.gamePaused;
870 level.gamePaused = false;
871 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
872 level.processThinkers(GameLevel::FrameTime);
873 level.gamePaused = wasPaused;
874 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
878 // ////////////////////////////////////////////////////////////////////////// //
879 private float currFrameDelta; // so level renderer can properly interpolate the player
880 private GameLevel::IVec2D camPrev, camCurr;
881 private GameLevel::IVec2D camShake;
882 private GameLevel::IVec2D viewCameraPos;
885 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
890 viewCameraPos.x = pos.x;
891 viewCameraPos.y = pos.y;
897 // call `recalcCameraCoords()` to get real camera coords after this
898 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
899 // check if camera is moved too far, and teleport it
901 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
902 abs(camCurr.y-pos.y)/global.scale >= 16*4))
904 teleportCameraAt(pos);
906 camPrev.x = camCurr.x;
907 camPrev.y = camCurr.y;
911 camShake.x = level.shakeDir.x*global.scale;
912 camShake.y = level.shakeDir.y*global.scale;
916 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
917 currFrameDelta = frameDelta;
918 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
919 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
921 viewCameraPos.x += camShake.x;
922 viewCameraPos.y += camShake.y;
926 GameLevel::SavedKeyState savedKeyState;
928 final void pauseGame () {
929 if (!level.gamePaused) {
930 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
931 level.gamePaused = true;
932 global.pauseAllSounds();
937 final void unpauseGame () {
938 if (level.gamePaused) {
939 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
940 level.gamePaused = false;
941 level.gameShowHelp = false;
942 level.gameHelpScreen = 0;
943 //lastThinkerTime = 0;
944 global.resumeAllSounds();
946 pauseRequested = false;
947 helpRequested = false;
952 final void beforeNewFrame (bool frameSkip) {
955 level.disablePlayerThink = true;
958 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
959 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
960 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
962 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
963 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
964 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
965 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
967 level.disablePlayerThink = false;
973 if (!level.gamePaused) {
974 // save seeds for afterframe processing
976 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
977 debugMovement.otherSeed = global.globalOtherSeed;
978 debugMovement.roomSeed = global.globalRoomSeed;
982 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
984 #ifdef BIGGER_REPLAY_DATA
985 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
986 debugMovement.keypresses.length += 1;
987 level.keysSaveState(debugMovement.keypresses[$-1]);
988 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
989 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
993 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
994 #ifdef BIGGER_REPLAY_DATA
995 if (debugMovement.keypos < debugMovement.keypresses.length) {
996 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
997 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
998 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
999 ++debugMovement.keypos;
1005 auto code = debugMovement.getKey(out kbidx, out down);
1006 if (code == DebugSessionMovement::END_OF_RECORD) {
1007 // do this in main loop, so we can view totals
1011 if (code == DebugSessionMovement::END_OF_FRAME) {
1014 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1015 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1023 final void afterNewFrame (bool frameSkip) {
1024 if (!replayFastForward) replaySkipFrame = 0;
1026 if (level.gamePaused) return;
1028 if (!level.gamePaused) {
1029 if (doGameSavingPlaying != Replay.None) {
1030 if (doGameSavingPlaying == Replay.Saving) {
1031 replayFastForward = false; // just in case
1032 #ifndef BIGGER_REPLAY_DATA
1033 debugMovement.addEndOfFrame();
1035 auto stt = GetTickCount();
1036 if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1037 } else if (doGameSavingPlaying == Replay.Replaying) {
1038 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1039 replaySkipFrame = 1;
1045 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1046 //SoundSystem.UpdateSounds();
1048 //if (!freeRide) level.fixCamera();
1049 setNewCameraPos(level.viewStart);
1051 prevCameraX = currCameraX;
1052 prevCameraY = currCameraY;
1053 currCameraX = level.cameraX;
1054 currCameraY = level.cameraY;
1055 // disable camera interpolation if the screen is shaking
1056 if (level.shakeX|level.shakeY) {
1057 prevCameraX = currCameraX;
1058 prevCameraY = currCameraY;
1061 // disable camera interpolation if it moves too far away
1062 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1063 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1065 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1067 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1068 pauseRequested = false;
1070 if (helpRequested) {
1071 helpRequested = false;
1072 level.gameShowHelp = true;
1073 level.gameHelpScreen = 0;
1076 if (!showHelp) showHelp = true;
1083 final void interFrame (float frameDelta) {
1084 if (!config.interpolateMovement) return;
1085 recalcCameraCoords(frameDelta);
1089 final void cameraTeleportedCB () {
1090 teleportCameraAt(level.viewStart);
1091 recalcCameraCoords(0);
1095 // ////////////////////////////////////////////////////////////////////////// //
1097 final void setColorByIdx (bool isset, int col) {
1099 // missed collision: red
1100 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1101 } else if (col == -999) {
1102 // superfluous collision: blue
1103 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1104 } else if (col <= 0) {
1105 // no collision: yellow
1106 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1107 } else if (col > 0) {
1109 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1114 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1116 CollisionMask cm = CollisionMask.Create(frm, false);
1118 int scale = global.config.scale;
1119 int bx0, by0, bx1, by1;
1120 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1121 Video.color = 0x7f_00_00_ff;
1122 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1123 if (!cm.isEmptyMask) {
1124 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1125 foreach (int iy; 0..cm.height) {
1126 foreach (int ix; 0..cm.width) {
1127 int v = cm.mask[ix, iy];
1128 foreach (int dx; 0..32) {
1131 Video.color = 0x3f_00_ff_00;
1132 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1141 foreach (int iy; 0..frm.tex.height) {
1142 foreach (int ix; 0..(frm.tex.width+31)/31) {
1143 foreach (int dx; 0..32) {
1145 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1146 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1147 setColorByIdx(true, col);
1148 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1150 Video.color = 0xaf_00_ff_00;
1152 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1158 if (frm.bw > 0 && frm.bh > 0) {
1159 setColorByIdx(true, col);
1160 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1161 Video.color = 0xff_00_00;
1162 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1171 // ////////////////////////////////////////////////////////////////////////// //
1172 transient int drawStats;
1173 transient array!int statsTopItem;
1176 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1177 auto sa = string(a.objName).toUpperCase;
1178 auto sb = string(b.objName).toUpperCase;
1183 final int getStatsTopItem () {
1184 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1188 final void setStatsTopItem (int val) {
1189 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1190 statsTopItem[drawStats] = val;
1194 final void resetStatsTopItem () {
1199 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1200 sprStore.loadFont('sFontSmall');
1206 final int calcStatsVisItems () {
1209 statsDrawGetStartPosLoadFont(currX, currY);
1210 int endY = level.viewHeight-(currY*2);
1211 return max(1, endY/sprStore.getFontHeight(scale));
1215 int getStatsItemCount () {
1216 switch (drawStats) {
1217 case 2: return level.stats.totalKills.length;
1218 case 3: return level.stats.totalDeaths.length;
1219 case 4: return level.stats.totalCollected.length;
1225 final void statsMoveUp () {
1226 int count = getStatsItemCount();
1227 if (count < 0) return;
1228 int visItems = calcStatsVisItems();
1229 if (count <= visItems) { resetStatsTopItem(); return; }
1230 int top = getStatsTopItem();
1232 setStatsTopItem(top-1);
1236 final void statsMoveDown () {
1237 int count = getStatsItemCount();
1238 if (count < 0) return;
1239 int visItems = calcStatsVisItems();
1240 if (count <= visItems) { resetStatsTopItem(); return; }
1241 int top = getStatsTopItem();
1242 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1243 top = clamp(top+1, 0, count-visItems);
1244 setStatsTopItem(top);
1248 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1249 arr.sort(&totalsNameCmpCB);
1253 statsDrawGetStartPosLoadFont(currX, currY);
1255 int endY = level.viewHeight-(currY*2);
1256 int visItems = calcStatsVisItems();
1258 if (arr.length <= visItems) resetStatsTopItem();
1260 int topItem = getStatsTopItem();
1264 Video.color = 0x3f_ff_ff_00;
1265 auto spr = sprStore['sPageUp'];
1266 spr.frames[0].tex.blitAt(currX-28, currY, scale);
1269 // "downscroll" mark
1270 if (topItem+visItems < arr.length) {
1271 Video.color = 0x3f_ff_ff_00;
1272 auto spr = sprStore['sPageDown'];
1273 spr.frames[0].tex.blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1276 Video.color = 0xff_ff_00;
1277 int hiColor = 0x00_ff_00;
1278 int hiColor1 = 0xf_ff_ff;
1281 while (it < arr.length && visItems-- > 0) {
1282 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);
1283 currY += sprStore.getFontHeight(scale);
1289 void drawStatsScreen () {
1290 int deathCount, killCount, collectCount;
1292 sprStore.loadFont('sFontSmall');
1294 Video.color = 0xff_ff_ff;
1295 level.drawTextAtS3Centered(240-2-8, "ESC-RETURN F10-QUIT CTRL+DEL-SUICIDE");
1296 level.drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
1298 Video.color = 0xff_ff_00;
1299 int hiColor = 0x00_ff_00;
1301 switch (drawStats) {
1302 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1303 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1304 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1307 if (drawStats > 1) {
1309 foreach (ref auto i; statsTopItem) i = 0;
1314 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1315 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1316 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1322 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1323 currY += sprStore.getFontHeight(scale);
1325 int gw = level.stats.gamesWon;
1326 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1327 currY += sprStore.getFontHeight(scale);
1329 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1330 currY += sprStore.getFontHeight(scale);
1332 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1333 currY += sprStore.getFontHeight(scale);
1335 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1336 currY += sprStore.getFontHeight(scale);
1338 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1339 currY += sprStore.getFontHeight(scale);
1341 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1342 currY += sprStore.getFontHeight(scale);
1344 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1345 currY += sprStore.getFontHeight(scale);
1347 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1348 currY += sprStore.getFontHeight(scale);
1350 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1351 currY += sprStore.getFontHeight(scale);
1353 int gs = level.stats.totalGhostSummoned;
1354 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1355 currY += sprStore.getFontHeight(scale);
1357 currY += sprStore.getFontHeight(scale);
1358 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1359 currY += sprStore.getFontHeight(scale);
1364 if (Video.frameTime == 0) {
1366 Video.requestRefresh();
1371 if (level.framesProcessedFromLastClear < 1) return;
1372 calcMouseMapCoords();
1374 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1375 Video.clearScreen();
1376 Video.stencil = false;
1377 Video.color = 0xff_ff_ff;
1378 Video.textureFiltering = false;
1379 // don't touch framebuffer alpha
1380 Video.colorMask = Video::CMask.Colors;
1382 Video::ScissorRect scsave;
1383 bool doRestoreGL = false;
1386 if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1388 Video.getScissor(scsave);
1389 Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1390 Video.glPushMatrix();
1391 Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1395 if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1397 float scx = float(Video.screenWidth)/float(level.viewWidth);
1398 float scy = float(Video.screenHeight)/float(level.viewHeight);
1399 float scale = fmin(scx, scy);
1400 int calcedW = trunc(level.viewWidth*scale);
1401 int calcedH = trunc(level.viewHeight*scale);
1402 Video.getScissor(scsave);
1403 int ofsx = (Video.screenWidth-calcedW)/2;
1404 int ofsy = (Video.screenHeight-calcedH)/2;
1405 Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1406 Video.glPushMatrix();
1407 Video.glTranslate(ofsx, ofsy);
1408 Video.glScale(scale, scale);
1411 //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1412 //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1416 level.viewOffsetX = 0;
1417 level.viewOffsetY = 0;
1418 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1421 float scx = float(Video.screenWidth)/float(level.viewWidth);
1422 float scy = float(Video.screenHeight)/float(level.viewHeight);
1423 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1428 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1430 if (level.gamePaused && showHelp != 2) {
1431 if (mouseLevelX != int.min) {
1432 int scale = level.global.scale;
1433 if (renderMouseRect) {
1434 Video.color = 0xcf_ff_ff_00;
1435 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1437 if (renderMouseTile) {
1438 Video.color = 0xaf_ff_00_00;
1439 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1444 switch (doGameSavingPlaying) {
1446 Video.color = 0x7f_00_ff_00;
1447 sprStore.loadFont('sFont');
1448 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1450 case Replay.Replaying:
1451 if (level.player && !level.player.dead) {
1452 Video.color = 0x7f_ff_00_00;
1453 sprStore.loadFont('sFont');
1454 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1455 int th = sprStore.getFontHeight(2);
1456 if (replayFastForward) {
1457 sprStore.loadFont('sFontSmall');
1458 string sstr = va("x%d", replayFastForwardSpeed+1);
1459 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1464 if (saveGameSession) {
1465 Video.color = 0x7f_ff_7f_00;
1466 sprStore.loadFont('sFont');
1467 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1473 if (level.player && level.player.dead && !showHelp) {
1475 Video.color = 0x8f_00_00_00;
1476 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1481 if (true /*level.inWinCutscene == 0*/) {
1482 Video.color = 0xff_ff_ff;
1483 sprStore.loadFont('sFontSmall');
1484 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1486 "PRESS $PAY TO RESTART GAME\n"~
1488 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1490 "TOTAL PLAYING TIME: |%s|"~
1492 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1493 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1494 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1496 GameLevel.time2str(level.stats.playingTime)
1498 kmsg = global.expandString(kmsg);
1499 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1506 Video.color = 0xff_7f_00;
1507 sprStore.loadFont('sFontSmall');
1508 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1509 auto spf = smask.frames[maskFrame];
1510 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1512 spf.bx, spf.by, spf.bw, spf.bh,
1513 (spf.maskEmpty ? "TAN" : "ONA"),
1514 (spf.precise ? "TAN" : "ONA")),
1517 //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1518 //writeln("pos=(", maskSX, ",", maskSY, ")");
1519 int scale = global.config.scale;
1520 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1521 int mapX = xofs/scale+maskSX;
1522 int mapY = yofs/scale+maskSY;
1525 writeln("==== tiles ====");
1527 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1528 if (t.spectral || !t.isInstanceAlive) return false;
1529 Video.color = 0x7f_ff_00_00;
1530 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);
1531 auto tsf = t.getSpriteFrame();
1533 auto spf = smask.frames[maskFrame];
1534 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1535 int mapX = xofs/global.config.scale+maskSX;
1536 int mapY = yofs/global.config.scale+maskSY;
1539 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1540 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1541 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1545 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1546 Video.color = 0x7f_ff_00_00;
1547 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);
1551 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1553 Video.color = 0xaf_ff_ff_ff;
1554 spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1555 Video.color = 0xff_ff_00;
1556 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1560 int fx0, fy0, fx1, fy1;
1561 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1562 Video.color = 0x7f_00_00_ff;
1563 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1569 Video.color = 0x8f_00_00_00;
1570 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1572 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1577 Video.color = 0xff_ff_00;
1578 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1579 if (showHelp == 1) {
1580 int msx, msy, ww, wh;
1581 Video.getMousePos(out msx, out msy);
1582 Video.getRealWindowSize(out ww, out wh);
1583 if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1584 sprStore.loadFont('sFontSmall');
1585 Video.color = 0xff_ff_00;
1586 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1587 "F1: show this help\n"~
1589 "K : redefine keys\n"~
1590 "I : toggle interpolaion\n"~
1591 "N : create some blood\n"~
1592 "R : generate a new level\n"~
1593 "F : toggle \"Frozen Area\"\n"~
1594 "X : resurrect player\n"~
1595 "Q : teleport to exit\n"~
1596 "D : teleport to damel\n"~
1598 "C : cheat flags menu\n"~
1599 "P : cheat pickup menu\n"~
1600 "E : cheat enemy menu\n"~
1601 "Enter: cheat items menu\n"~
1603 "TAB: toggle 'freeroam' mode\n"~
1608 if (level) level.renderPauseOverlay();
1612 //SoundSystem.UpdateSounds();
1614 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1617 Video.setScissor(scsave);
1618 Video.glPopMatrix();
1623 Video.color = 0xaf_ff_ff_ff;
1624 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1629 // ////////////////////////////////////////////////////////////////////////// //
1630 transient bool gameJustOver;
1631 transient bool waitingForPayRestart;
1634 final void calcMouseMapCoords () {
1635 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1636 mouseLevelX = int.min;
1637 mouseLevelY = int.min;
1640 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1641 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1642 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1646 final void onEvent (ref event_t evt) {
1647 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1649 if (evt.type == ev_winfocus) {
1650 if (level && !evt.focused) {
1655 //writeln("FOCUS!");
1656 Video.getMousePos(out mouseX, out mouseY);
1661 if (evt.type == ev_mouse) {
1664 calcMouseMapCoords();
1667 if (evt.type == ev_keydown && evt.keycode == K_F12) {
1668 if (level) toggleFullscreen();
1672 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1673 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1674 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1677 if (evt.type == ev_keydown) {
1678 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1679 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1680 renderMouseTile = evt.bCtrl;
1681 renderMouseRect = evt.bAlt;
1684 if (evt.type == ev_keyup) {
1685 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1686 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1687 renderMouseTile = evt.bCtrl;
1688 renderMouseRect = evt.bAlt;
1691 if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1693 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1694 int newScale = evt.keycode-48;
1695 if (global.config.scale != newScale) {
1696 global.config.scale = newScale;
1699 cameraTeleportedCB();
1706 if (evt.type == ev_mouse) {
1707 maskSX = evt.x/global.config.scale;
1708 maskSY = evt.y/global.config.scale;
1711 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1712 maskFrame = max(0, maskFrame-1);
1715 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1716 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1725 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1727 if (saveOptionsDG) saveOptionsDG();
1728 saveOptionsDG = none;
1730 //SoundSystem.UpdateSounds(); // just in case
1731 if (global.hasSpectacles) level.pickedSpectacles();
1734 optionsPane.onEvent(evt);
1738 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1739 if (evt.type == ev_keydown) {
1740 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1741 switch (evt.keycode) {
1742 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1743 case K_F2: if (showHelp != 2) unpauseGame(); return;
1744 case K_F10: Video.requestQuit(); return;
1745 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1747 case K_UPARROW: case K_PAD8:
1748 if (drawStats) statsMoveUp();
1751 case K_DOWNARROW: case K_PAD2:
1752 if (drawStats) statsMoveDown();
1755 case K_LEFTARROW: case K_PAD4:
1756 if (level && showHelp == 2 && level.gameShowHelp) {
1757 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1761 case K_RIGHTARROW: case K_PAD6:
1762 if (level && showHelp == 2 && level.gameShowHelp) {
1763 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1777 resetFramesAndForceOne();
1783 if (/*evt.bCtrl &&*/ showHelp != 2) {
1793 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1794 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1795 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1796 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1797 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1798 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1799 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1800 //case K_j: global.hasJordans = !global.hasJordans; return;
1802 if (/*evt.bCtrl &&*/ showHelp != 2) {
1803 level.resurrectPlayer();
1808 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1809 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1810 if (evt.bAlt && level.player && level.player.dead) {
1811 saveGameSession = false;
1812 replayGameSession = true;
1816 if (/*evt.bCtrl &&*/ showHelp != 2) {
1817 if (evt.bShift) global.idol = false;
1818 level.generateLevel();
1819 level.centerViewAtPlayer();
1820 teleportCameraAt(level.viewStart);
1821 resetFramesAndForceOne();
1825 global.toggleMusic();
1828 if (/*evt.bCtrl &&*/ showHelp != 2) {
1829 if (level.allExits.length) {
1830 level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1836 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1837 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1839 level.teleportPlayerTo(damsel.ix, damsel.iy);
1845 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1849 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1852 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1855 level.teleportPlayerTo(obj.ix, obj.iy-4);
1861 if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1862 if (level && mouseLevelX != int.min) {
1863 int scale = level.global.scale;
1864 int mapX = mouseLevelX;
1865 int mapY = mouseLevelY;
1866 level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1872 if (evt.bCtrl && showHelp != 2) {
1873 if (level && mouseLevelX != int.min) {
1874 int scale = level.global.scale;
1875 int mapX = mouseLevelX;
1876 int mapY = mouseLevelY;
1877 level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1883 if (evt.bCtrl && showHelp != 2) {
1884 if (level && mouseLevelX != int.min) {
1885 int scale = level.global.scale;
1886 int mapX = mouseLevelX;
1887 int mapY = mouseLevelY;
1888 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1892 if (evt.bAlt && showHelp != 2) {
1893 if (level && mouseLevelX != int.min) {
1894 int scale = level.global.scale;
1895 int mapX = mouseLevelX;
1896 int mapY = mouseLevelY;
1897 level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1903 if (level && mouseLevelX != int.min) {
1904 int scale = level.global.scale;
1905 int mapX = mouseLevelX;
1906 int mapY = mouseLevelY;
1909 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1910 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1911 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1915 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1916 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1922 if (/*evt.bAlt &&*/ showHelp != 2) {
1923 auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1924 //if (obj) obj.monkey = monkey;
1926 //playSound('sndThump');
1932 case K_DELETE: // suicide
1933 if (doGameSavingPlaying == Replay.None) {
1934 if (level.player && !level.player.dead && evt.bCtrl) {
1935 global.hasAnkh = false;
1936 level.global.plife = 1;
1937 level.player.invincible = 0;
1938 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1939 if (xplo) xplo.suicide = true;
1946 if (level.player && !level.player.dead && evt.bAlt) {
1947 if (doGameSavingPlaying != Replay.None) {
1948 if (doGameSavingPlaying == Replay.Replaying) {
1950 } else if (doGameSavingPlaying == Replay.Saving) {
1951 saveGameMovement(dbgSessionMovementFileName, packit:true);
1953 doGameSavingPlaying = Replay.None;
1955 saveGameSession = false;
1956 replayGameSession = false;
1963 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1964 level.stats.setMoneyCheat();
1965 level.stats.addMoney(10000);
1971 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1972 if (level.player && level.player.dead) {
1973 //Video.requestQuit();
1975 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1977 #ifdef QUIT_DOUBLE_ESC
1978 if (++escCount == 2) Video.requestQuit();
1981 pauseRequested = true;
1987 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
1988 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1989 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1992 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1995 if (!level.player || !level.player.dead) {
1996 gameJustOver = false;
1997 } else if (level.player && level.player.dead) {
1998 if (!gameJustOver) {
2000 gameJustOver = true;
2001 waitingForPayRestart = true;
2002 level.clearKeysPressRelease();
2003 if (doGameSavingPlaying == Replay.None) {
2004 stopReplaying(); // just in case
2008 replayFastForward = false;
2009 if (doGameSavingPlaying == Replay.Saving) {
2010 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2011 doGameSavingPlaying = Replay.None;
2012 //clearGameMovement();
2013 saveGameSession = false;
2014 replayGameSession = false;
2017 if (evt.type == ev_keydown || evt.type == ev_keyup) {
2018 bool down = (evt.type == ev_keydown);
2019 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2020 if (down && evt.keycode == K_f) {
2022 if (replayFastForwardSpeed != 4) {
2023 replayFastForwardSpeed = 4;
2024 replayFastForward = true;
2026 replayFastForward = !replayFastForward;
2029 replayFastForwardSpeed = 2;
2030 replayFastForward = !replayFastForward;
2034 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2035 foreach (int kbidx, int kval; global.config.keybinds) {
2036 if (kval && kval == evt.keycode) {
2037 #ifndef BIGGER_REPLAY_DATA
2038 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2040 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2044 if (level.player && level.player.dead) {
2045 if (down && evt.keycode == K_r && evt.bAlt) {
2046 saveGameSession = false;
2047 replayGameSession = true;
2050 if (down && evt.keycode == K_s && evt.bAlt) {
2051 bool wasSaveReq = saveGameSession;
2052 stopReplaying(); // just in case
2053 saveGameSession = !wasSaveReq;
2054 replayGameSession = false;
2057 if (replayGameSession) {
2058 stopReplaying(); // just in case
2059 saveGameSession = false;
2060 replayGameSession = false;
2061 loadGameMovement(dbgSessionMovementFileName);
2062 loadGame(dbgSessionStateFileName);
2063 doGameSavingPlaying = Replay.Replaying;
2066 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2067 if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2068 if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2069 if (waitingForPayRestart) {
2070 level.isKeyReleased(GameConfig::Key.Pay);
2071 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2073 level.isKeyPressed(GameConfig::Key.Pay);
2074 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2075 auto doSave = saveGameSession;
2076 stopReplaying(); // just in case
2077 level.clearKeysPressRelease();
2078 level.restartGame();
2079 level.generateNormalLevel();
2081 saveGameSession = false;
2082 replayGameSession = false;
2083 writeln("DBG: saving game session...");
2084 clearGameMovement();
2085 doGameSavingPlaying = Replay.Saving;
2086 saveGame(dbgSessionStateFileName);
2087 //saveGameMovement(dbgSessionMovementFileName);
2098 void levelExited () {
2104 void initializeVideo () {
2105 Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2106 if (Video.realStencilBits < 8) {
2107 Video.closeScreen();
2108 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2110 if (!Video.framebufferHasAlpha) {
2111 Video.closeScreen();
2112 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!");
2114 if (fullscreen) Video.hideMouseCursor();
2118 void toggleFullscreen () {
2119 Video.showMouseCursor();
2120 Video.closeScreen();
2121 fullscreen = !fullscreen;
2126 final void runGameLoop () {
2127 Video.frameTime = 0; // unlimited FPS
2128 lastThinkerTime = 0;
2130 sprStore = SpawnObject(SpriteStore);
2131 sprStore.bDumpLoaded = false;
2133 bgtileStore = SpawnObject(BackTileStore);
2134 bgtileStore.bDumpLoaded = false;
2136 level = SpawnObject(GameLevel);
2137 level.setup(global, sprStore, bgtileStore);
2139 level.BuildYear = BuildYear;
2140 level.BuildMonth = BuildMonth;
2141 level.BuildDay = BuildDay;
2142 level.BuildHour = BuildHour;
2143 level.BuildMin = BuildMin;
2145 level.global = global;
2146 level.sprStore = sprStore;
2147 level.bgtileStore = bgtileStore;
2150 //level.stats.introViewed = 0;
2152 if (level.stats.introViewed == 0) {
2153 startMode = StartMode.Intro;
2154 writeln("FORCED INTRO");
2156 //writeln("INTRO VIWED: ", level.stats.introViewed);
2157 if (level.global.config.skipIntro) startMode = StartMode.Title;
2160 level.onBeforeFrame = &beforeNewFrame;
2161 level.onAfterFrame = &afterNewFrame;
2162 level.onInterFrame = &interFrame;
2163 level.onLevelExitedCB = &levelExited;
2164 level.onCameraTeleported = &cameraTeleportedCB;
2167 maskSX = -0x0ff_fff;
2169 smask = sprStore['sExplosionMask'];
2173 sprStore.loadFont('sFontSmall');
2175 level.viewWidth = 320*3;
2176 level.viewHeight = 240*3;
2178 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2179 //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2180 fullscreen = global.config.startFullscreen;
2183 //SoundSystem.SwapStereo = config.swapStereo;
2184 SoundSystem.NumChannels = 32;
2185 SoundSystem.MaxHearingDistance = 12000;
2186 //SoundSystem.DopplerFactor = 1.0f;
2187 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2188 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2189 SoundSystem.ReferenceDistance = 16.0f*4;
2190 SoundSystem.MaxDistance = 16.0f*(5*10);
2192 SoundSystem.Initialize();
2193 if (!SoundSystem.IsInitialized) {
2194 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2195 global.soundDisabled = true;
2196 global.musicDisabled = true;
2198 global.fixVolumes();
2200 level.restartGame(); // this will NOT generate a new level
2205 texTigerEye = GLTexture.Load("teye0.png");
2207 if (global.cheatEndGameSequence) {
2208 level.winTime = 12*60+42;
2209 level.stats.money = 6666;
2210 switch (global.cheatEndGameSequence) {
2211 case 1: default: level.startWinCutscene(); break;
2212 case 2: level.startWinCutsceneVolcano(); break;
2213 case 3: level.startWinCutsceneWinFall(); break;
2216 switch (startMode) {
2217 case StartMode.Title: level.restartTitle(); break;
2218 case StartMode.Intro: level.restartIntro(); break;
2219 case StartMode.Stars: level.restartStarsRoom(); break;
2220 case StartMode.Sun: level.restartSunRoom(); break;
2221 case StartMode.Moon: level.restartMoonRoom(); break;
2223 level.generateNormalLevel();
2224 if (startMode == StartMode.Dead) {
2225 level.player.dead = true;
2226 level.player.visible = false;
2232 //global.rope = 666;
2233 //global.bombs = 666;
2235 //global.globalRoomSeed = 871520037;
2236 //global.globalOtherSeed = 1047036290;
2238 //level.createTitleRoom();
2239 //level.createTrans4Room();
2240 //level.createOlmecRoom();
2241 //level.generateLevel();
2243 //level.centerViewAtPlayer();
2244 teleportCameraAt(level.viewStart);
2245 //writeln(Video.swapInterval);
2247 Video.runEventLoop();
2248 Video.showMouseCursor();
2249 Video.closeScreen();
2250 SoundSystem.Shutdown();
2252 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2260 // ////////////////////////////////////////////////////////////////////////// //
2261 // duplicates are not allowed!
2262 final void checkGameObjNames () {
2263 array!(class!Object) known;
2265 int classCount = 0, namedCount = 0;
2266 foreach AllClasses(Object, out cc) {
2267 auto gn = GetClassGameObjName(cc);
2269 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2270 auto nid = NameToInt(gn);
2271 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));
2277 writeln(classCount, " classes, ", namedCount, " game object classes.");
2281 // ////////////////////////////////////////////////////////////////////////// //
2282 #include "timelimit.vc"
2283 //const int TimeLimitDate = 2018232;
2286 void performTimeCheck () {
2287 #ifdef DISABLE_TIME_CHECK
2289 if (TigerEye) return;
2292 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2295 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2297 int tldate = tm.year*1000+tm.yday;
2299 if (tldate > TimeLimitDate) {
2300 level.maxPlayingTime = 24;
2302 //writeln("*** days left: ", TimeLimitDate-tldate);
2308 void setupCheats () {
2311 startMode = StartMode.Alive;
2312 global.currLevel = 2;
2313 global.scumGenShop = true;
2314 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2315 //global.config.scale = 1;
2318 startMode = StartMode.Alive;
2319 global.currLevel = 13;
2320 global.config.scale = 2;
2323 startMode = StartMode.Alive;
2324 global.currLevel = 13;
2325 global.config.scale = 1;
2326 global.cityOfGold = true;
2329 startMode = StartMode.Alive;
2330 global.currLevel = 5;
2331 global.genBlackMarket = true;
2334 startMode = StartMode.Alive;
2335 global.currLevel = 2;
2336 global.scumGenShop = true;
2337 global.scumGenShopType = GameGlobal::ShopType.Weapon;
2338 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2339 //global.config.scale = 1;
2342 //startMode = StartMode.Intro;
2345 global.currLevel = 2;
2346 startMode = StartMode.Alive;
2349 global.currLevel = 5;
2350 startMode = StartMode.Alive;
2351 global.scumGenLake = true;
2352 global.config.scale = 1;
2355 startMode = StartMode.Alive;
2356 global.cheatCanSkipOlmec = true;
2357 global.currLevel = 16;
2358 //global.currLevel = 5;
2359 //global.currLevel = 13;
2360 //global.config.scale = 1;
2362 //startMode = StartMode.Dead;
2363 //startMode = StartMode.Title;
2364 //startMode = StartMode.Stars;
2365 //startMode = StartMode.Sun;
2366 startMode = StartMode.Moon;
2368 //global.scumGenSacrificePit = true;
2369 //global.scumAlwaysSacrificeAltar = true;
2371 // first lush jungle level
2372 //global.levelType = 1;
2374 global.scumGenCemetary = true;
2376 //global.idol = false;
2377 //global.currLevel = 5;
2379 //global.isTunnelMan = true;
2382 //global.currLevel = 5;
2383 //global.scumGenLake = true;
2385 //global.currLevel = 5;
2386 //global.currLevel = 9;
2387 //global.currLevel = 13;
2388 //global.currLevel = 14;
2389 //global.cheatEndGameSequence = 1;
2392 //global.currLevel = 6;
2393 global.scumGenAlienCraft = true;
2394 global.currLevel = 9;
2395 //global.scumGenYetiLair = true;
2396 //global.genBlackMarket = true;
2397 //startDead = false;
2398 startMode = StartMode.Alive;
2401 global.cheatCanSkipOlmec = true;
2402 global.currLevel = 15;
2403 startMode = StartMode.Alive;
2406 global.scumGenShop = true;
2407 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2408 global.scumGenShopType = GameGlobal::ShopType.Craps;
2409 //global.scumGenShopType = 6; // craps
2410 //global.scumGenShopType = 7; // kissing
2412 //global.scumAlwaysSacrificeAltar = true;
2416 void setupSeeds () {
2420 // ////////////////////////////////////////////////////////////////////////// //
2422 checkGameObjNames();
2424 appSetName("k8spelunky");
2425 config = SpawnObject(GameConfig);
2426 global = SpawnObject(GameGlobal);
2427 global.config = config;
2428 config.heroType = GameConfig::Hero.Spelunker;
2430 global.randomizeSeedAll();
2432 fillCheatPickupList();
2433 fillCheatItemsList();
2434 fillCheatEnemiesList();
2437 loadKeyboardBindings();