1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2018, Ketmar Dark
5 * This file is part of Spelunky.
7 * You can redistribute and/or modify Spelunky, including its source code, under
8 * the terms of the Spelunky User License.
10 * Spelunky is distributed in the hope that it will be entertaining and useful,
11 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
13 * The Spelunky User License should be available in "Game Information", which
14 * can be found in the Resource Explorer, or as an external file called COPYING.
15 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17 **********************************************************************************/
25 //#define QUIT_DOUBLE_ESC
31 //#define BIGGER_REPLAY_DATA
33 // ////////////////////////////////////////////////////////////////////////// //
34 #include "mapent/0all.vc"
35 #include "PlayerPawn.vc"
36 #include "PlayerPowerup.vc"
37 #include "GameLevel.vc"
40 // ////////////////////////////////////////////////////////////////////////// //
41 #include "uisimple.vc"
44 // ////////////////////////////////////////////////////////////////////////// //
45 class DebugSessionMovement : Object;
47 #ifdef BIGGER_REPLAY_DATA
48 array!(GameLevel::SavedKeyState) keypresses;
50 array!ubyte keypresses; // on each frame
52 GameConfig playconfig;
55 transient int otherSeed, roomSeed;
58 override void Destroy () {
60 keypresses.length = 0;
65 final void resetReplay () {
70 #ifndef BIGGER_REPLAY_DATA
71 final void addKey (int kbidx, bool down) {
72 if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
73 keypresses[$] = kbidx|(down ? 0x80 : 0);
77 final void addEndOfFrame () {
88 final int getKey (out int kbidx, out bool down) {
89 if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
90 if (keypos >= keypresses.length) return END_OF_RECORD;
91 ubyte b = keypresses[keypos++];
92 if (b == 0xff) return END_OF_FRAME;
100 // ////////////////////////////////////////////////////////////////////////// //
101 class TempOptionsKeys : Object;
103 int[16*GameConfig::MaxActionBinds] keybinds;
106 // ////////////////////////////////////////////////////////////////////////// //
109 transient string dbgSessionStateFileName = "debug_game_session_state";
110 transient string dbgSessionMovementFileName = "debug_game_session_movement";
112 GLTexture texTigerEye;
116 SpriteStore sprStore;
117 BackTileStore bgtileStore;
129 StartMode startMode = StartMode.Title;
130 bool freeRide = false;
131 bool switchInterpolator;
134 bool replayFastForward = false;
135 int replayFastForwardSpeed = 2;
136 bool saveGameSession = false;
137 bool replayGameSession = false;
143 Replay doGameSavingPlaying = Replay.None;
144 float saveMovementLastTime = 0;
145 DebugSessionMovement debugMovement;
146 GameStats origStats; // for replaying
147 GameConfig origConfig; // for replaying
148 int origRoomSeed, origOtherSeed;
154 transient int maskSX, maskSY;
155 transient SpriteImage smask;
156 transient int maskFrame;
160 // ////////////////////////////////////////////////////////////////////////// //
161 final void saveKeyboardBindings () {
162 auto tok = SpawnObject(TempOptionsKeys);
163 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
164 appSaveOptions(tok, "keybindings");
169 final void loadKeyboardBindings () {
170 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
172 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
178 // ////////////////////////////////////////////////////////////////////////// //
179 void saveGameOptions () {
180 appSaveOptions(global.config, "config");
184 void loadGameOptions () {
185 auto cfg = appLoadOptions(GameConfig, "config");
187 auto oldHero = config.heroType;
188 auto tok = SpawnObject(TempOptionsKeys);
189 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
190 delete global.config;
193 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
195 writeln("config loaded");
196 global.restartMusic();
198 //config.heroType = GameConfig::Hero.Spelunker;
199 config.heroType = oldHero;
202 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
206 // ////////////////////////////////////////////////////////////////////////// //
207 void saveGameStats () {
208 if (level.stats) appSaveOptions(level.stats, "stats");
212 void loadGameStats () {
213 auto stats = appLoadOptions(GameStats, "stats");
218 if (!level.stats) level.stats = SpawnObject(GameStats);
219 level.stats.global = global;
223 // ////////////////////////////////////////////////////////////////////////// //
224 struct UIPaneSaveInfo {
226 UIPane::SaveInfo nfo;
229 transient UIPane optionsPane; // either options, or binding editor
231 transient GameLevel::IVec2D optionsPaneOfs;
232 transient void delegate () saveOptionsDG;
234 transient array!UIPaneSaveInfo optionsPaneState;
237 final void saveCurrentPane () {
238 if (!optionsPane || !optionsPane.id) return;
241 if (optionsPane.id == 'CheatFlags') {
242 if (instantGhost && level.ghostTimeLeft > 0) {
243 level.ghostTimeLeft = 1;
247 foreach (ref auto psv; optionsPaneState) {
248 if (psv.id == optionsPane.id) {
249 optionsPane.saveState(psv.nfo);
254 optionsPaneState.length += 1;
255 optionsPaneState[$-1].id = optionsPane.id;
256 optionsPane.saveState(optionsPaneState[$-1].nfo);
260 final void restoreCurrentPane () {
261 if (optionsPane) optionsPane.setupHotkeys(); // why not?
262 if (!optionsPane || !optionsPane.id) return;
263 foreach (ref auto psv; optionsPaneState) {
264 if (psv.id == optionsPane.id) {
265 optionsPane.restoreState(psv.nfo);
272 // ////////////////////////////////////////////////////////////////////////// //
273 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
274 if (!it.tagClass) return;
275 if (class!MapObject(it.tagClass)) {
276 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
277 it.owner.closeMe = true;
282 // ////////////////////////////////////////////////////////////////////////// //
283 transient array!(class!MapObject) cheatItemsList;
286 final void fillCheatItemsList () {
287 cheatItemsList.length = 0;
288 cheatItemsList[$] = ItemProjectileArrow;
289 cheatItemsList[$] = ItemWeaponShotgun;
290 cheatItemsList[$] = ItemWeaponAshShotgun;
291 cheatItemsList[$] = ItemWeaponPistol;
292 cheatItemsList[$] = ItemWeaponMattock;
293 cheatItemsList[$] = ItemWeaponMachete;
294 cheatItemsList[$] = ItemWeaponWebCannon;
295 cheatItemsList[$] = ItemWeaponSceptre;
296 cheatItemsList[$] = ItemWeaponBow;
297 cheatItemsList[$] = ItemBones;
298 cheatItemsList[$] = ItemFakeBones;
299 cheatItemsList[$] = ItemFishBone;
300 cheatItemsList[$] = ItemRock;
301 cheatItemsList[$] = ItemJar;
302 cheatItemsList[$] = ItemSkull;
303 cheatItemsList[$] = ItemGoldenKey;
304 cheatItemsList[$] = ItemGoldIdol;
305 cheatItemsList[$] = ItemCrystalSkull;
306 cheatItemsList[$] = ItemShellSingle;
307 cheatItemsList[$] = ItemChest;
308 cheatItemsList[$] = ItemCrate;
309 cheatItemsList[$] = ItemLockedChest;
310 cheatItemsList[$] = ItemDice;
314 final UIPane createCheatItemsPane () {
315 if (!level.player) return none;
317 UIPane pane = SpawnObject(UIPane);
319 pane.sprStore = sprStore;
321 pane.width = 320*3-64;
322 pane.height = 240*3-64;
324 foreach (auto ipk; cheatItemsList) {
325 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
329 //optionsPaneOfs.x = 100;
330 //optionsPaneOfs.y = 50;
336 // ////////////////////////////////////////////////////////////////////////// //
337 transient array!(class!MapObject) cheatEnemiesList;
340 final void fillCheatEnemiesList () {
341 cheatEnemiesList.length = 0;
342 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
343 cheatEnemiesList[$] = EnemyBat;
344 cheatEnemiesList[$] = EnemySpiderHang;
345 cheatEnemiesList[$] = EnemySpider;
346 cheatEnemiesList[$] = EnemySnake;
347 cheatEnemiesList[$] = EnemyCaveman;
348 cheatEnemiesList[$] = EnemySkeleton;
349 cheatEnemiesList[$] = MonsterShopkeeper;
350 cheatEnemiesList[$] = EnemyZombie;
351 cheatEnemiesList[$] = EnemyVampire;
352 cheatEnemiesList[$] = EnemyFrog;
353 cheatEnemiesList[$] = EnemyGreenFrog;
354 cheatEnemiesList[$] = EnemyFireFrog;
355 cheatEnemiesList[$] = EnemyMantrap;
356 cheatEnemiesList[$] = EnemyScarab;
357 cheatEnemiesList[$] = EnemyFloater;
358 cheatEnemiesList[$] = EnemyBlob;
359 cheatEnemiesList[$] = EnemyMonkey;
360 cheatEnemiesList[$] = EnemyGoldMonkey;
361 cheatEnemiesList[$] = EnemyAlien;
362 cheatEnemiesList[$] = EnemyYeti;
363 cheatEnemiesList[$] = EnemyHawkman;
364 cheatEnemiesList[$] = EnemyUFO;
365 cheatEnemiesList[$] = EnemyYetiKing;
369 final UIPane createCheatEnemiesPane () {
370 if (!level.player) return none;
372 UIPane pane = SpawnObject(UIPane);
374 pane.sprStore = sprStore;
376 pane.width = 320*3-64;
377 pane.height = 240*3-64;
379 foreach (auto ipk; cheatEnemiesList) {
380 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
384 //optionsPaneOfs.x = 100;
385 //optionsPaneOfs.y = 50;
391 // ////////////////////////////////////////////////////////////////////////// //
392 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
395 final void fillCheatPickupList () {
396 cheatPickupList.length = 0;
397 cheatPickupList[$] = ItemPickupBombBag;
398 cheatPickupList[$] = ItemPickupBombBox;
399 cheatPickupList[$] = ItemPickupPaste;
400 cheatPickupList[$] = ItemPickupRopePile;
401 cheatPickupList[$] = ItemPickupShellBox;
402 cheatPickupList[$] = ItemPickupAnkh;
403 cheatPickupList[$] = ItemPickupCape;
404 cheatPickupList[$] = ItemPickupJetpack;
405 cheatPickupList[$] = ItemPickupUdjatEye;
406 cheatPickupList[$] = ItemPickupCrown;
407 cheatPickupList[$] = ItemPickupKapala;
408 cheatPickupList[$] = ItemPickupParachute;
409 cheatPickupList[$] = ItemPickupCompass;
410 cheatPickupList[$] = ItemPickupSpectacles;
411 cheatPickupList[$] = ItemPickupGloves;
412 cheatPickupList[$] = ItemPickupMitt;
413 cheatPickupList[$] = ItemPickupJordans;
414 cheatPickupList[$] = ItemPickupSpringShoes;
415 cheatPickupList[$] = ItemPickupSpikeShoes;
416 cheatPickupList[$] = ItemPickupTeleporter;
420 final UIPane createCheatPickupsPane () {
421 if (!level.player) return none;
423 UIPane pane = SpawnObject(UIPane);
425 pane.sprStore = sprStore;
427 pane.width = 320*3-64;
428 pane.height = 240*3-64;
430 foreach (auto ipk; cheatPickupList) {
431 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
435 //optionsPaneOfs.x = 100;
436 //optionsPaneOfs.y = 50;
442 // ////////////////////////////////////////////////////////////////////////// //
443 transient int instantGhost;
445 final UIPane createCheatFlagsPane () {
446 UIPane pane = SpawnObject(UIPane);
447 pane.id = 'CheatFlags';
448 pane.sprStore = sprStore;
450 pane.width = 320*3-64;
451 pane.height = 240*3-64;
455 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
456 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
457 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
458 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
459 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
460 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
461 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
462 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
463 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
464 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
465 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
466 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
467 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
468 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
469 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
470 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
471 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
472 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
474 optionsPaneOfs.x = 100;
475 optionsPaneOfs.y = 50;
481 final UIPane createOptionsPane () {
482 UIPane pane = SpawnObject(UIPane);
484 pane.sprStore = sprStore;
486 pane.width = 320*3-64;
487 pane.height = 240*3-64;
489 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
490 //!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.");
491 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
492 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).");
493 UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
495 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
496 mm.names[$] = "SILENCE";
497 mm.names[$] = "RESTART";
498 mm.names[$] = "DON'T TOUCH";
500 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
501 //mm.names[$] = "SILENCE";
502 mm.names[$] = "RESTART";
503 mm.names[$] = "DON'T TOUCH";
505 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
506 herotype.names[$] = "SPELUNKY GUY";
507 herotype.names[$] = "DAMSEL";
508 herotype.names[$] = "TUNNEL MAN";
510 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
511 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.");
512 // i won't implement this
513 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.");
515 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
518 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
521 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
523 //!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.");
524 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.");
525 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.");
527 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.");
528 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
529 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!");
530 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.");
532 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
533 whiptype.names[$] = "NORMAL";
534 whiptype.names[$] = "LONG";
535 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
537 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.");
538 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
539 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.");
540 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.");
541 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
543 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
544 plrlit.names[$] = "NEVER";
545 plrlit.names[$] = "FORCED DARKNESS";
546 plrlit.names[$] = "ALWAYS";
548 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'.");
549 rdark.names[$] = "NEVER";
550 rdark.names[$] = "DEFAULT";
551 rdark.names[$] = "ALWAYS";
553 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.");
555 rghost.getNameCB = delegate string (int val) {
556 if (val < 0) return "INSTANT";
557 if (val == 0) return "NEVER";
558 if (val < 120) return va("%d SEC", val);
559 if (val%60 == 0) return va("%d MIN", val/60);
560 if (val%60 == 30) return va("%d.5 MIN", val/60);
561 return va("%d MIN, %d SEC", val/60, val%60);
564 UICheckBox.Create(pane, &config.ghostRandom, "RANDOM GHOST DELAY", "THIS OPTION WILL RANDOMIZE THE DELAY UNTIL THE GHOST APPEARS AFTER THE TIME LIMIT ABOVE 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.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
566 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.");
567 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.");
568 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.");
569 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
570 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
571 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.");
572 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.");
573 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
574 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
575 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
577 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
578 rstl.names[$] = "RANDOM";
579 rstl.names[$] = "NORMAL";
580 rstl.names[$] = "BIZARRE";
582 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
583 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
584 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
586 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.");
587 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?");
589 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
591 swstereo.onValueChanged = delegate void (int newval) {
592 SoundSystem.SwapStereo = newval;
596 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
597 rmusonoff.onValueChanged = delegate void (int newval) {
598 global.restartMusic();
601 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
603 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
604 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
606 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
607 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
609 saveOptionsDG = delegate void () {
610 writeln("saving options");
613 optionsPaneOfs.x = 42;
614 optionsPaneOfs.y = 0;
620 final void createBindingsControl (UIPane pane, int keyidx) {
623 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
624 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
625 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
626 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
627 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
628 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
629 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
630 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
631 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
632 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
633 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
636 int arridx = GameConfig.getKeyIndex(keyidx);
637 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
641 final UIPane createBindingsPane () {
642 UIPane pane = SpawnObject(UIPane);
643 pane.id = 'KeyBindings';
644 pane.sprStore = sprStore;
646 pane.width = 320*3-64;
647 pane.height = 240*3-64;
649 createBindingsControl(pane, GameConfig::Key.Left);
650 createBindingsControl(pane, GameConfig::Key.Right);
651 createBindingsControl(pane, GameConfig::Key.Up);
652 createBindingsControl(pane, GameConfig::Key.Down);
653 createBindingsControl(pane, GameConfig::Key.Jump);
654 createBindingsControl(pane, GameConfig::Key.Run);
655 createBindingsControl(pane, GameConfig::Key.Attack);
656 createBindingsControl(pane, GameConfig::Key.Switch);
657 createBindingsControl(pane, GameConfig::Key.Pay);
658 createBindingsControl(pane, GameConfig::Key.Bomb);
659 createBindingsControl(pane, GameConfig::Key.Rope);
661 saveOptionsDG = delegate void () {
662 writeln("saving keys");
663 saveKeyboardBindings();
665 optionsPaneOfs.x = 120;
666 optionsPaneOfs.y = 140;
672 // ////////////////////////////////////////////////////////////////////////// //
673 void clearGameMovement () {
674 debugMovement = SpawnObject(DebugSessionMovement);
675 debugMovement.playconfig = SpawnObject(GameConfig);
676 debugMovement.playconfig.copyGameplayConfigFrom(config);
677 debugMovement.resetReplay();
681 void saveGameMovement (string fname) {
682 if (debugMovement) appSaveOptions(debugMovement, fname);
683 saveMovementLastTime = GetTickCount();
687 void loadGameMovement (string fname) {
688 delete debugMovement;
689 debugMovement = appLoadOptions(DebugSessionMovement, fname);
690 debugMovement.resetReplay();
693 origStats = level.stats;
694 origStats.global = none;
695 level.stats = SpawnObject(GameStats);
696 level.stats.global = global;
699 config = debugMovement.playconfig;
700 global.config = config;
701 origRoomSeed = global.globalRoomSeed;
702 origOtherSeed = global.globalOtherSeed;
703 writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
708 void stopReplaying () {
710 writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
711 global.globalRoomSeed = origRoomSeed;
712 global.globalOtherSeed = origOtherSeed;
714 delete debugMovement;
715 saveGameSession = false;
716 replayGameSession = false;
717 doGameSavingPlaying = Replay.None;
720 origStats.global = global;
721 level.stats = origStats;
727 global.config = origConfig;
733 // ////////////////////////////////////////////////////////////////////////// //
734 final bool saveGame (string gmname) {
735 return appSaveOptions(level, gmname);
739 final bool loadGame (string gmname) {
740 auto olddel = ImmediateDelete;
741 ImmediateDelete = false;
743 auto stats = level.stats;
746 auto lvl = appLoadOptions(GameLevel, gmname);
748 //lvl.global.config = config;
753 global = level.global;
754 global.config = config;
756 level.sprStore = sprStore;
757 level.bgtileStore = bgtileStore;
760 level.onBeforeFrame = &beforeNewFrame;
761 level.onAfterFrame = &afterNewFrame;
762 level.onInterFrame = &interFrame;
764 level.viewWidth = Video.screenWidth;
765 level.viewHeight = Video.screenHeight;
768 level.centerViewAtPlayer();
769 teleportCameraAt(level.viewStart);
771 recalcCameraCoords(0);
776 level.stats.global = level.global;
778 ImmediateDelete = olddel;
779 CollectGarbage(true); // destroy delayed objects too
784 // ////////////////////////////////////////////////////////////////////////// //
785 float lastThinkerTime;
786 int replaySkipFrame = 0;
789 final void onTimePasses () {
790 float curTime = GetTickCount();
791 if (lastThinkerTime > 0) {
792 if (curTime < lastThinkerTime) {
793 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
794 lastThinkerTime = curTime;
797 if (replayFastForward && replaySkipFrame) {
799 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
802 level.processThinkers(curTime-lastThinkerTime);
804 lastThinkerTime = curTime;
808 // ////////////////////////////////////////////////////////////////////////// //
809 private float currFrameDelta; // so level renderer can properly interpolate the player
810 private GameLevel::IVec2D camPrev, camCurr;
811 private GameLevel::IVec2D camShake;
812 private GameLevel::IVec2D viewCameraPos;
815 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
820 viewCameraPos.x = pos.x;
821 viewCameraPos.y = pos.y;
827 // call `recalcCameraCoords()` to get real camera coords after this
828 final void setNewCameraPos (const ref GameLevel::IVec2D pos) {
829 // check if camera is moved too far, and teleport it
830 if (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
831 abs(camCurr.y-pos.y)/global.scale >= 16*4)
833 teleportCameraAt(pos);
835 camPrev.x = camCurr.x;
836 camPrev.y = camCurr.y;
840 camShake.x = level.shakeDir.x*global.scale;
841 camShake.y = level.shakeDir.y*global.scale;
845 final void recalcCameraCoords (float frameDelta) {
846 currFrameDelta = frameDelta;
847 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
848 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
850 // update sound listener position (it is either at player position, or in viewport center)
854 (viewCameraPos.x+level.viewWidth/2.0)/global.scale,
855 (viewCameraPos.y+level.viewHeight/2.0)/global.scale
858 viewCameraPos.x += camShake.x;
859 viewCameraPos.y += camShake.y;
860 lv = vector(float(level.player.xCenter), float(level.player.yCenter));
862 SoundSystem.ListenerOrigin = lv;
863 SoundSystem.UpdateSounds();
867 GameLevel::SavedKeyState savedKeyState;
869 final void pauseGame () {
870 if (!level.gamePaused) {
871 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
872 level.gamePaused = true;
877 final void unpauseGame () {
878 if (level.gamePaused) {
879 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
880 level.gamePaused = false;
882 //lastThinkerTime = 0;
887 final void beforeNewFrame (bool frameSkip) {
889 level.disablePlayerThink = true;
892 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
893 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
894 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
896 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
897 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
898 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
899 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
901 level.disablePlayerThink = false;
905 if (level.isKeyDown(PlayerPawn::KeyLeft)) level.player.fltx -= delta;
906 if (level.isKeyDown(PlayerPawn::KeyRight)) level.player.fltx += delta;
907 if (level.isKeyDown(PlayerPawn::KeyUp)) level.player.flty -= delta;
908 if (level.isKeyDown(PlayerPawn::KeyDown)) level.player.flty += delta;
911 if (!level.gamePaused) {
912 // save seeds for afterframe processing
914 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
915 debugMovement.otherSeed = global.globalOtherSeed;
916 debugMovement.roomSeed = global.globalRoomSeed;
920 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
922 #ifdef BIGGER_REPLAY_DATA
923 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
924 debugMovement.keypresses.length += 1;
925 level.keysSaveState(debugMovement.keypresses[$-1]);
926 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
927 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
931 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
932 #ifdef BIGGER_REPLAY_DATA
933 if (debugMovement.keypos < debugMovement.keypresses.length) {
934 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
935 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
936 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
937 ++debugMovement.keypos;
943 auto code = debugMovement.getKey(out kbidx, out down);
944 if (code == DebugSessionMovement::END_OF_RECORD) {
945 // do this in main loop, so we can view totals
949 if (code == DebugSessionMovement::END_OF_FRAME) {
952 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
953 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
961 final void afterNewFrame (bool frameSkip) {
962 if (!replayFastForward) replaySkipFrame = 0;
964 if (level.gamePaused) return;
966 if (!level.gamePaused) {
967 if (doGameSavingPlaying != Replay.None) {
968 if (doGameSavingPlaying == Replay.Saving) {
969 replayFastForward = false; // just in case
970 #ifndef BIGGER_REPLAY_DATA
971 debugMovement.addEndOfFrame();
973 auto stt = GetTickCount();
974 if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
975 } else if (doGameSavingPlaying == Replay.Replaying) {
976 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
983 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
984 //SoundSystem.UpdateSounds();
986 if (!freeRide) level.fixCamera();
987 setNewCameraPos(level.viewStart);
989 prevCameraX = currCameraX;
990 prevCameraY = currCameraY;
991 currCameraX = level.cameraX;
992 currCameraY = level.cameraY;
993 // disable camera interpolation if the screen is shaking
994 if (level.shakeX|level.shakeY) {
995 prevCameraX = currCameraX;
996 prevCameraY = currCameraY;
999 // disable camera interpolation if it moves too far away
1000 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1001 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1003 if (switchInterpolator) {
1004 switchInterpolator = false;
1005 config.interpolateMovement = !config.interpolateMovement;
1007 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0); // recalc camera coords
1009 if (pauseRequested) {
1010 pauseRequested = false;
1012 if (!showHelp) showHelp = true;
1018 final void interFrame (float frameDelta) {
1019 if (!config.interpolateMovement) return;
1020 recalcCameraCoords(frameDelta);
1024 // ////////////////////////////////////////////////////////////////////////// //
1026 final void setColorByIdx (bool isset, int col) {
1028 // missed collision: red
1029 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1030 } else if (col == -999) {
1031 // superfluous collision: blue
1032 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1033 } else if (col <= 0) {
1034 // no collision: yellow
1035 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1036 } else if (col > 0) {
1038 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1043 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1045 CollisionMask cm = CollisionMask.Create(frm, false);
1047 int scale = global.config.scale;
1048 int bx0, by0, bx1, by1;
1049 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1050 Video.color = 0x7f_00_00_ff;
1051 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1052 if (!cm.isEmptyMask) {
1053 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1054 foreach (int iy; 0..cm.height) {
1055 foreach (int ix; 0..cm.width) {
1056 int v = cm.mask[ix, iy];
1057 foreach (int dx; 0..32) {
1060 Video.color = 0x3f_00_ff_00;
1061 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1070 foreach (int iy; 0..frm.tex.height) {
1071 foreach (int ix; 0..(frm.tex.width+31)/31) {
1072 foreach (int dx; 0..32) {
1074 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1075 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1076 setColorByIdx(true, col);
1077 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1079 Video.color = 0xaf_00_ff_00;
1081 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1087 if (frm.bw > 0 && frm.bh > 0) {
1088 setColorByIdx(true, col);
1089 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1090 Video.color = 0xff_00_00;
1091 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1100 // ////////////////////////////////////////////////////////////////////////// //
1101 transient int drawStats;
1102 transient array!int statsTopItem;
1105 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1106 auto sa = string(a.objName);
1107 auto sb = string(b.objName);
1112 final int getStatsTopItem () {
1113 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1117 final void setStatsTopItem (int val) {
1118 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1119 statsTopItem[drawStats] = val;
1123 final void resetStatsTopItem () {
1128 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1129 sprStore.loadFont('sFontSmall');
1135 final int calcStatsVisItems () {
1138 statsDrawGetStartPosLoadFont(currX, currY);
1139 int endY = Video.screenHeight-(currY*2);
1140 return max(1, endY/sprStore.getFontHeight(scale));
1144 int getStatsItemCount () {
1145 switch (drawStats) {
1146 case 2: return level.stats.totalKills.length;
1147 case 3: return level.stats.totalDeaths.length;
1148 case 4: return level.stats.totalCollected.length;
1154 final void statsMoveUp () {
1155 int count = getStatsItemCount();
1156 if (count < 0) return;
1157 int visItems = calcStatsVisItems();
1158 if (count <= visItems) { resetStatsTopItem(); return; }
1159 int top = getStatsTopItem();
1161 setStatsTopItem(top-1);
1165 final void statsMoveDown () {
1166 int count = getStatsItemCount();
1167 if (count < 0) return;
1168 int visItems = calcStatsVisItems();
1169 if (count <= visItems) { resetStatsTopItem(); return; }
1170 int top = getStatsTopItem();
1171 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1172 top = clamp(top+1, 0, count-visItems);
1173 setStatsTopItem(top);
1177 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1178 GameStats.sortTotalsList(arr, &totalsNameCmpCB);
1182 statsDrawGetStartPosLoadFont(currX, currY);
1184 int endY = Video.screenHeight-(currY*2);
1185 int visItems = calcStatsVisItems();
1187 if (arr.length <= visItems) resetStatsTopItem();
1189 int topItem = getStatsTopItem();
1193 Video.color = 0x3f_ff_ff_00;
1194 auto spr = sprStore['sPageUp'];
1195 spr.frames[0].tex.blitAt(currX-24, currY, scale);
1198 // "downscroll" mark
1199 if (topItem+visItems < arr.length) {
1200 Video.color = 0x3f_ff_ff_00;
1201 auto spr = sprStore['sPageDown'];
1202 spr.frames[0].tex.blitAt(currX-24, endY/*-sprStore.getFontHeight(scale)*/, scale);
1205 Video.color = 0xff_ff_00;
1206 int hiColor = 0x00_ff_00;
1207 int hiColor1 = 0xf_ff_ff;
1210 while (it < arr.length && visItems-- > 0) {
1211 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);
1212 currY += sprStore.getFontHeight(scale);
1218 void drawStatsScreen () {
1219 int deathCount, killCount, collectCount;
1221 switch (drawStats) {
1222 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1223 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1224 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1226 if (drawStats > 1) {
1228 foreach (ref auto i; statsTopItem) i = 0;
1233 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1234 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1235 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1237 Video.color = 0xff_ff_00;
1238 int hiColor = 0x00_ff_00;
1239 sprStore.loadFont('sFontSmall');
1245 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1246 currY += sprStore.getFontHeight(scale);
1248 int gw = level.stats.gamesWon;
1249 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1250 currY += sprStore.getFontHeight(scale);
1252 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1253 currY += sprStore.getFontHeight(scale);
1255 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1256 currY += sprStore.getFontHeight(scale);
1258 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1259 currY += sprStore.getFontHeight(scale);
1261 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1262 currY += sprStore.getFontHeight(scale);
1264 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1265 currY += sprStore.getFontHeight(scale);
1267 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1268 currY += sprStore.getFontHeight(scale);
1270 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1271 currY += sprStore.getFontHeight(scale);
1273 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1274 currY += sprStore.getFontHeight(scale);
1276 int gs = level.stats.totalGhostSummoned;
1277 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1278 currY += sprStore.getFontHeight(scale);
1280 currY += sprStore.getFontHeight(scale);
1281 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1282 currY += sprStore.getFontHeight(scale);
1287 if (Video.frameTime == 0) {
1289 Video.requestRefresh();
1293 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1294 Video.clearScreen();
1295 Video.stencil = false;
1296 Video.color = 0xff_ff_ff;
1297 Video.textureFiltering = false;
1298 // don't touch framebuffer alpha
1299 Video.colorMask = Video::CMask.Colors;
1301 if (level.gamePaused) {
1302 //level.renderWithOfs(trunc(currCameraX), trunc(currCameraY), 1.0);
1303 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1306 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1308 auto ltex = bgtileStore.lightTexture('ltx256', 128);
1309 auto ctex = bgtileStore.circleTexture('ctx256', 128);
1311 auto oblend = Video.blendMode;
1312 Video.blendMode = Video::BlendMode.Normal;
1313 //Video.blendMode = Video::Blend.Blend;
1314 //Video.blendMode = Video::Blend.Particle;
1315 Video.color = 0x00_ff_ff_ff;
1318 // stenciling (it works)
1319 Video.stencil = true;
1320 Video.stencilFunc(Video::StencilFunc.Always, 1);
1321 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
1322 Video.alphaTestFunc = Video::AlphaFunc.Equal;
1323 Video.alphaTestVal = 1;
1324 ctex.tex.blitAt(10, 10);
1325 Video.alphaTestFunc = Video::AlphaFunc.Always;
1328 Video.stencil = false;
1329 Video.clearScreen();
1330 Video.stencil = true;
1333 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
1334 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
1336 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1339 Video.blendMode = Video::BlendMode.Normal;
1340 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1342 Video.blendMode = Video::BlendMode.Particle;
1343 //Video.blendMode = Video::Blend.Highlight;
1344 Video.alphaTestFunc = Video::AlphaFunc.Less;
1345 Video.alphaTestVal = 0.8;
1347 ltex.tex.blitAt(100, 100);
1348 ltex.tex.blitAt(150, 150);
1351 Video.stencil = false;
1352 Video.alphaTestFunc = Video::AlphaFunc.Always;
1353 Video.alphaTestVal = 1;
1356 //native final static void stencilOp (StencilOp sfail, StencilOp dpfail, optional StencilOp dppass);
1357 //native final static void stencilFunc (StencilFunc func, int refval, optional int mask);
1359 //!!ctex.tex.blitAt(10, 10);
1360 //ltex.tex.blitAt(60, 60);
1361 //ltex.tex.blitAt(260, 260);
1362 Video.blendMode = oblend;
1363 //level.renderWithOfs(cameraX, cameraY, currFrameDelta);
1365 auto oblend = Video.blendMode;
1366 Video.blendMode = Video::Blend.Filter;
1367 Video.color = 0x00_00_3f;
1368 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1369 Video.blendMode = oblend;
1376 auto ltex = bgtileStore.lightTexture('ltx512', 512);
1378 // set screen alpha to min
1379 Video.colorMask = Video::CMask.Alpha;
1380 Video.blendMode = Video::BlendMode.None;
1381 Video.color = 0x7f_ff_ff_ff;
1382 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1383 //Video.colorMask = Video::CMask.All;
1386 // also, stencil 'em, so we can filter dark areas
1387 Video.textureFiltering = true;
1388 Video.stencil = true;
1389 Video.stencilFunc(Video::StencilFunc.Always, 1);
1390 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
1391 Video.alphaTestFunc = Video::AlphaFunc.Greater;
1392 Video.alphaTestVal = 0.05;
1393 Video.color = 0xff_ff_ff;
1394 Video.blendFunc = Video::BlendFunc.Max;
1395 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
1396 Video.colorMask = Video::CMask.Alpha;
1397 ltex.tex.blitAt(Video.screenWidth/2-256, Video.screenHeight/2-256, 0.5);
1398 //ltex.tex.blitAt(120, 120, 0.5);
1399 Video.textureFiltering = false;
1401 // modify only lit parts
1402 Video.stencilFunc(Video::StencilFunc.Equal, 1);
1403 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
1404 // multiply framebuffer colors by framebuffer alpha
1405 Video.color = 0xff_ff_ff; // it doesn't matter
1406 Video.blendFunc = Video::BlendFunc.Add;
1407 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
1408 Video.colorMask = Video::CMask.Colors;
1409 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1411 // filter unlit parts
1412 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
1413 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
1414 Video.blendFunc = Video::BlendFunc.Add;
1415 Video.blendMode = Video::BlendMode.Filter;
1416 Video.colorMask = Video::CMask.Colors;
1417 Video.color = 0x00_00_18;
1418 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1421 Video.blendFunc = Video::BlendFunc.Add;
1422 Video.blendMode = Video::BlendMode.Normal;
1423 Video.colorMask = Video::CMask.All;
1424 Video.alphaTestFunc = Video::AlphaFunc.Always;
1425 Video.stencil = false;
1429 switch (doGameSavingPlaying) {
1431 Video.color = 0x7f_00_ff_00;
1432 sprStore.loadFont('sFont');
1433 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1435 case Replay.Replaying:
1436 if (level.player && !level.player.dead) {
1437 Video.color = 0x7f_ff_00_00;
1438 sprStore.loadFont('sFont');
1439 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1440 int th = sprStore.getFontHeight(2);
1441 if (replayFastForward) {
1442 sprStore.loadFont('sFontSmall');
1443 string sstr = va("x%d", replayFastForwardSpeed+1);
1444 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1449 if (saveGameSession) {
1450 Video.color = 0x7f_ff_7f_00;
1451 sprStore.loadFont('sFont');
1452 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1458 if (level.player && level.player.dead && !showHelp) {
1460 Video.color = 0x8f_00_00_00;
1461 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1466 if (true /*level.inWinCutscene == 0*/) {
1467 Video.color = 0xff_ff_ff;
1468 sprStore.loadFont('sFontSmall');
1469 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1471 "PRESS $PAY TO RESTART GAME\n"~
1473 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1475 "TOTAL PLAYING TIME: |%s|"~
1477 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1478 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1479 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1481 GameLevel.time2str(level.stats.playingTime)
1483 kmsg = global.expandString(kmsg);
1484 sprStore.renderMultilineTextCentered(Video.screenWidth/2, int.min, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1491 Video.color = 0xff_7f_00;
1492 sprStore.loadFont('sFontSmall');
1493 sprStore.renderText(8, Video.screenHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1494 auto spf = smask.frames[maskFrame];
1495 sprStore.renderText(8, Video.screenHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1497 spf.bx, spf.by, spf.bw, spf.bh,
1498 (spf.maskEmpty ? "TAN" : "ONA"),
1499 (spf.precise ? "TAN" : "ONA")),
1502 //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1503 //writeln("pos=(", maskSX, ",", maskSY, ")");
1504 int scale = global.config.scale;
1505 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1506 int mapX = xofs/scale+maskSX;
1507 int mapY = yofs/scale+maskSY;
1510 writeln("==== tiles ====");
1512 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1513 if (t.spectral || !t.isInstanceAlive) return false;
1514 Video.color = 0x7f_ff_00_00;
1515 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);
1516 auto tsf = t.getSpriteFrame();
1518 auto spf = smask.frames[maskFrame];
1519 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1520 int mapX = xofs/global.config.scale+maskSX;
1521 int mapY = yofs/global.config.scale+maskSY;
1524 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1525 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1526 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1530 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1531 Video.color = 0x7f_ff_00_00;
1532 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);
1536 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1538 Video.color = 0xaf_ff_ff_ff;
1539 spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1540 Video.color = 0xff_ff_00;
1541 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1545 int fx0, fy0, fx1, fy1;
1546 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1547 Video.color = 0x7f_00_00_ff;
1548 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1554 Video.color = 0x8f_00_00_00;
1555 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1557 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1562 Video.color = 0xff_ff_00;
1563 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1564 if (showHelp == 1) {
1565 sprStore.loadFont('sFontSmall');
1566 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1567 "F1: show this help\n"~
1569 "K : redefine keys\n"~
1570 "I : toggle interpolaion\n"~
1571 "N : create some blood\n"~
1572 "R : generate a new level\n"~
1573 "F : toggle \"Frozen Area\"\n"~
1574 "X : resurrect player\n"~
1575 "Q : teleport to exit\n"~
1576 "D : teleport to damel\n"~
1578 "C : cheat flags menu\n"~
1579 "P : cheat pickup menu\n"~
1580 "E : cheat enemy menu\n"~
1581 "Enter: cheat items menu\n"~
1583 "TAB: toggle 'freeroam' mode\n"~
1589 //SoundSystem.UpdateSounds();
1591 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1594 Video.color = 0xaf_ff_ff_ff;
1595 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1600 // ////////////////////////////////////////////////////////////////////////// //
1601 transient bool gameJustOver;
1602 transient bool waitingForPayRestart;
1605 final void onEvent (ref event_t evt) {
1606 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1608 if (evt.type == ev_winfocus) {
1609 if (level && !evt.focused) {
1616 if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1618 if (evt.type == ev_keydown && evt.keycode == "1") { global.config.scale = 1; return; }
1619 if (evt.type == ev_keydown && evt.keycode == "2") { global.config.scale = 2; return; }
1620 if (evt.type == ev_keydown && evt.keycode == "3") { global.config.scale = 3; return; }
1621 if (evt.type == ev_keydown && evt.keycode == "4") { global.config.scale = 4; return; }
1624 if (evt.type == ev_mouse) {
1625 maskSX = evt.x/global.config.scale;
1626 maskSY = evt.y/global.config.scale;
1629 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1630 maskFrame = max(0, maskFrame-1);
1633 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1634 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1643 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1645 if (saveOptionsDG) saveOptionsDG();
1646 saveOptionsDG = none;
1648 SoundSystem.UpdateSounds(); // just in case
1649 if (global.hasSpectacles) level.pickedSpectacles();
1652 optionsPane.onEvent(evt);
1656 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1657 if (evt.type == ev_keydown) {
1658 switch (evt.keycode) {
1659 case K_F1: if (showHelp > 1) showHelp = 1; else unpauseGame(); return;
1660 case K_F10: Video.requestQuit(); return;
1661 case K_F12: showHelp = 3-showHelp; return;
1663 case K_UPARROW: case K_PAD8:
1664 if (drawStats) statsMoveUp();
1666 case K_DOWNARROW: case K_PAD2:
1667 if (drawStats) statsMoveDown();
1693 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1694 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1695 case K_c: optionsPane = createCheatFlagsPane(); restoreCurrentPane(); return;
1696 case K_p: optionsPane = createCheatPickupsPane(); restoreCurrentPane(); return;
1697 case K_ENTER: optionsPane = createCheatItemsPane(); restoreCurrentPane(); return;
1698 case K_e: optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); return;
1699 case K_TAB: freeRide = !freeRide; return;
1700 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1701 //case K_j: global.hasJordans = !global.hasJordans; return;
1702 case K_i: switchInterpolator = true; unpauseGame(); return;
1706 auto bomb = ItemBomb(level.MakeMapObject(level.player.ix, level.player.iy, 'oBomb'));
1707 if (bomb) bomb.armIt();
1709 level.resurrectPlayer();
1714 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1715 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1716 if (evt.bAlt && level.player && level.player.dead) {
1717 saveGameSession = false;
1718 replayGameSession = true;
1722 if (evt.bCtrl) global.idol = false;
1723 level.generateLevel();
1724 lastThinkerTime = 0;
1725 level.centerViewAtPlayer();
1726 teleportCameraAt(level.viewStart);
1729 global.toggleMusic();
1732 level.pickedSpectacles();
1735 global.config.useFrozenRegion = !global.config.useFrozenRegion;
1739 if (level.allExits.length) {
1740 level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1746 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1748 level.teleportPlayerTo(damsel.ix, damsel.iy);
1755 auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1757 level.teleportPlayerTo(obj.ix, obj.iy-4);
1764 auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1766 level.teleportPlayerTo(obj.ix, obj.iy);
1773 auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1774 //if (obj) obj.monkey = monkey;
1776 //playSound('sndThump');
1782 case K_DELETE: // suicide
1783 if (doGameSavingPlaying == Replay.None) {
1784 if (level.player && !level.player.dead && evt.bCtrl) {
1785 global.hasAnkh = false;
1786 level.global.plife = 1;
1787 level.player.invincible = 0;
1788 level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion');
1795 if (level.player && !level.player.dead && evt.bAlt) {
1796 if (doGameSavingPlaying != Replay.None) {
1797 if (doGameSavingPlaying == Replay.Replaying) {
1799 } else if (doGameSavingPlaying == Replay.Saving) {
1800 saveGameMovement(dbgSessionMovementFileName);
1802 doGameSavingPlaying = Replay.None;
1804 saveGameSession = false;
1805 replayGameSession = false;
1812 level.stats.setMoneyCheat();
1813 level.stats.addMoney(10000);
1818 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1819 if (level.player && level.player.dead) {
1820 //Video.requestQuit();
1822 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1824 #ifdef QUIT_DOUBLE_ESC
1825 if (++escCount == 2) Video.requestQuit();
1828 pauseRequested = true;
1833 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; return; }
1836 if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1839 if (!level.player || !level.player.dead) {
1840 gameJustOver = false;
1841 } else if (level.player && level.player.dead) {
1842 if (!gameJustOver) {
1844 gameJustOver = true;
1845 waitingForPayRestart = true;
1846 level.clearKeysPressRelease();
1847 if (doGameSavingPlaying == Replay.None) {
1848 stopReplaying(); // just in case
1852 replayFastForward = false;
1853 if (doGameSavingPlaying == Replay.Saving) {
1854 if (debugMovement) saveGameMovement(dbgSessionMovementFileName);
1855 doGameSavingPlaying = Replay.None;
1856 //clearGameMovement();
1857 saveGameSession = false;
1858 replayGameSession = false;
1861 if (evt.type == ev_keydown || evt.type == ev_keyup) {
1862 bool down = (evt.type == ev_keydown);
1863 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
1864 if (down && evt.keycode == K_f) {
1866 if (replayFastForwardSpeed != 4) {
1867 replayFastForwardSpeed = 4;
1868 replayFastForward = true;
1870 replayFastForward = !replayFastForward;
1873 replayFastForwardSpeed = 2;
1874 replayFastForward = !replayFastForward;
1878 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
1879 foreach (int kbidx, int kval; global.config.keybinds) {
1880 if (kval && kval == evt.keycode) {
1881 #ifndef BIGGER_REPLAY_DATA
1882 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
1884 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1888 if (level.player && level.player.dead) {
1889 if (down && evt.keycode == K_r && evt.bAlt) {
1890 saveGameSession = false;
1891 replayGameSession = true;
1894 if (down && evt.keycode == K_s && evt.bAlt) {
1895 bool wasSaveReq = saveGameSession;
1896 stopReplaying(); // just in case
1897 saveGameSession = !wasSaveReq;
1898 replayGameSession = false;
1901 if (replayGameSession) {
1902 stopReplaying(); // just in case
1903 saveGameSession = false;
1904 replayGameSession = false;
1905 loadGameMovement(dbgSessionMovementFileName);
1906 loadGame(dbgSessionStateFileName);
1907 doGameSavingPlaying = Replay.Replaying;
1909 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
1910 if (waitingForPayRestart) {
1911 level.isKeyReleased(GameConfig::Key.Pay);
1912 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
1914 level.isKeyPressed(GameConfig::Key.Pay);
1915 if (level.isKeyReleased(GameConfig::Key.Pay)) {
1916 auto doSave = saveGameSession;
1917 stopReplaying(); // just in case
1918 level.clearKeysPressRelease();
1919 level.restartGame();
1920 level.generateNormalLevel();
1922 saveGameSession = false;
1923 replayGameSession = false;
1924 writeln("DBG: saving game session...");
1925 clearGameMovement();
1926 doGameSavingPlaying = Replay.Saving;
1927 saveGame(dbgSessionStateFileName);
1928 saveGameMovement(dbgSessionMovementFileName);
1939 void levelExited () {
1945 final void runGameLoop () {
1946 Video.frameTime = 0; // unlimited FPS
1947 lastThinkerTime = 0;
1949 sprStore = SpawnObject(SpriteStore);
1950 sprStore.bDumpLoaded = false;
1952 bgtileStore = SpawnObject(BackTileStore);
1953 bgtileStore.bDumpLoaded = false;
1955 level = SpawnObject(GameLevel);
1956 level.setup(global, sprStore, bgtileStore);
1958 level.global = global;
1959 level.sprStore = sprStore;
1960 level.bgtileStore = bgtileStore;
1964 level.onBeforeFrame = &beforeNewFrame;
1965 level.onAfterFrame = &afterNewFrame;
1966 level.onInterFrame = &interFrame;
1967 level.onLevelExitedCB = &levelExited;
1970 maskSX = -0x0ff_fff;
1972 smask = sprStore['sExplosionMask'];
1976 sprStore.loadFont('sFontSmall');
1978 Video.swapInterval = (global.config.optVSync ? 1 : 0);
1979 Video.openScreen("Spelunky/VaVoom C", 320*3, 240*3);
1981 if (Video.realStencilBits < 8) {
1982 Video.closeScreen();
1983 FatalError("FATAL: no stencil buffer!");
1985 if (!Video.framebufferHasAlpha) {
1986 Video.closeScreen();
1987 FatalError("FATAL: no alpha channel in framebuffer!");
1990 //SoundSystem.SwapStereo = config.swapStereo;
1991 SoundSystem.DopplerFactor = 1.0f;
1992 SoundSystem.DopplerVelocity = 10000.0f;
1993 SoundSystem.RolloffFactor = 1.0f;
1994 //SoundSystem.ReferenceDistance = 32.0f; // The distance under which the volume for the source would normally drop by half (before being influenced by rolloff factor or AL_MAX_DISTANCE)
1995 SoundSystem.ReferenceDistance = 32.0f*5; // The distance under which the volume for the source would normally drop by half (before being influenced by rolloff factor or AL_MAX_DISTANCE)
1996 SoundSystem.MaxDistance = 800.0f*2; // Used with the Inverse Clamped Distance Model to set the distance where there will no longer be any attenuation of the source
1997 SoundSystem.NumChannels = 64;
1998 SoundSystem.Sound2DPos = vector(0, 0, -1);
2000 SoundSystem.Initialize();
2001 global.fixVolumes();
2003 level.viewWidth = Video.screenWidth;
2004 level.viewHeight = Video.screenHeight;
2006 level.restartGame(); // this will NOT generate a new level
2011 texTigerEye = GLTexture.Load("sprites/teye0.png");
2013 if (global.cheatEndGameSequence) {
2014 level.winTime = 12*60+42;
2015 level.stats.money = 6666;
2016 switch (global.cheatEndGameSequence) {
2017 case 1: default: level.startWinCutscene(); break;
2018 case 2: level.startWinCutsceneVolcano(); break;
2019 case 3: level.startWinCutsceneWinFall(); break;
2022 switch (startMode) {
2023 case StartMode.Title: level.restartTitle(); break;
2024 case StartMode.Stars: level.restartStarsRoom(); break;
2025 case StartMode.Sun: level.restartSunRoom(); break;
2026 case StartMode.Moon: level.restartMoonRoom(); break;
2028 level.generateNormalLevel();
2029 if (startMode == StartMode.Dead) {
2030 level.player.dead = true;
2031 level.player.visible = false;
2037 //global.rope = 666;
2038 //global.bombs = 666;
2040 //global.globalRoomSeed = 871520037;
2041 //global.globalOtherSeed = 1047036290;
2043 //level.createTitleRoom();
2044 //level.createTrans4Room();
2045 //level.createOlmecRoom();
2046 //level.generateLevel();
2048 //level.centerViewAtPlayer();
2049 teleportCameraAt(level.viewStart);
2050 //writeln(Video.swapInterval);
2052 Video.runEventLoop();
2053 Video.closeScreen();
2055 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName);
2060 level.clearObjects();
2064 // ////////////////////////////////////////////////////////////////////////// //
2065 // duplicates are not allowed!
2066 final void checkGameObjNames () {
2067 array!(class!Object) known;
2069 int classCount = 0, namedCount = 0;
2070 foreach AllClasses(Object, out cc) {
2071 auto gn = GetClassGameObjName(cc);
2073 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2074 auto nid = NameToInt(gn);
2075 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));
2081 writeln(classCount, " classes, ", namedCount, " game object classes.");
2085 // ////////////////////////////////////////////////////////////////////////// //
2086 #include "timelimit.vc"
2087 //const int TimeLimitDate = 2018232;
2090 void performTimeCheck () {
2091 #ifdef DISABLE_TIME_CHECK
2093 if (TigerEye) return;
2096 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2099 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2101 int tldate = tm.year*1000+tm.yday;
2103 if (tldate > TimeLimitDate) {
2104 level.maxPlayingTime = 24;
2106 //writeln("*** days left: ", TimeLimitDate-tldate);
2112 void setupCheats () {
2114 //startMode = StartMode.Dead;
2115 //startMode = StartMode.Title;
2116 //startMode = StartMode.Stars;
2117 //startMode = StartMode.Sun;
2118 startMode = StartMode.Moon;
2120 //global.scumGenSacrificePit = true;
2121 //global.scumAlwaysSacrificeAltar = true;
2123 // first lush jungle level
2124 //global.levelType = 1;
2126 global.scumGenCemetary = true;
2128 //global.idol = false;
2129 //global.currLevel = 5;
2131 //global.isTunnelMan = true;
2134 //global.currLevel = 5;
2135 //global.scumGenLake = true;
2137 //global.currLevel = 5;
2138 //global.currLevel = 9;
2139 //global.currLevel = 13;
2140 //global.currLevel = 14;
2141 //global.cheatEndGameSequence = 1;
2144 //global.currLevel = 6;
2145 global.scumGenAlienCraft = true;
2146 global.currLevel = 9;
2147 //global.scumGenYetiLair = true;
2148 //global.genBlackMarket = true;
2149 //startDead = false;
2150 startMode = StartMode.Alive;
2153 global.cheatCanSkipOlmec = true;
2154 global.currLevel = 15;
2155 startMode = StartMode.Alive;
2158 global.scumGenShop = true;
2159 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2160 global.scumGenShopType = GameGlobal::ShopType.Craps;
2161 //global.scumGenShopType = 6; // craps
2162 //global.scumGenShopType = 7; // kissing
2164 //global.scumAlwaysSacrificeAltar = true;
2168 void setupSeeds () {
2172 // ////////////////////////////////////////////////////////////////////////// //
2174 checkGameObjNames();
2176 appSetName("k8spelunky");
2177 config = SpawnObject(GameConfig);
2178 global = SpawnObject(GameGlobal);
2179 global.config = config;
2180 config.heroType = GameConfig::Hero.Spelunker;
2182 global.randomizeSeedAll();
2184 fillCheatPickupList();
2185 fillCheatItemsList();
2186 fillCheatEnemiesList();
2189 loadKeyboardBindings();