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
29 //#define BIGGER_REPLAY_DATA
31 // ////////////////////////////////////////////////////////////////////////// //
32 #include "mapent/0all.vc"
33 #include "PlayerPawn.vc"
34 #include "PlayerPowerup.vc"
35 #include "GameLevel.vc"
38 // ////////////////////////////////////////////////////////////////////////// //
39 #include "uisimple.vc"
42 // ////////////////////////////////////////////////////////////////////////// //
43 class DebugSessionMovement : Object;
45 #ifdef BIGGER_REPLAY_DATA
46 array!(GameLevel::SavedKeyState) keypresses;
48 array!ubyte keypresses; // on each frame
50 GameConfig playconfig;
53 transient int otherSeed, roomSeed;
56 override void Destroy () {
58 keypresses.length = 0;
63 final void resetReplay () {
68 #ifndef BIGGER_REPLAY_DATA
69 final void addKey (int kbidx, bool down) {
70 if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
71 keypresses[$] = kbidx|(down ? 0x80 : 0);
75 final void addEndOfFrame () {
86 final int getKey (out int kbidx, out bool down) {
87 if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
88 if (keypos >= keypresses.length) return END_OF_RECORD;
89 ubyte b = keypresses[keypos++];
90 if (b == 0xff) return END_OF_FRAME;
98 // ////////////////////////////////////////////////////////////////////////// //
99 class TempOptionsKeys : Object;
101 int[16*GameConfig::MaxActionBinds] keybinds;
104 // ////////////////////////////////////////////////////////////////////////// //
107 transient string dbgSessionStateFileName = "debug_game_session_state";
108 transient string dbgSessionMovementFileName = "debug_game_session_movement";
110 GLTexture texTigerEye;
114 SpriteStore sprStore;
115 BackTileStore bgtileStore;
118 int mouseX = int.min, mouseY = int.min;
119 int mouseLevelX = int.min, mouseLevelY = int.min;
120 bool renderMouseTile;
121 bool renderMouseRect;
132 StartMode startMode = StartMode.Title;
133 bool freeRide = false;
134 bool switchInterpolator;
137 bool replayFastForward = false;
138 int replayFastForwardSpeed = 2;
139 bool saveGameSession = false;
140 bool replayGameSession = false;
146 Replay doGameSavingPlaying = Replay.None;
147 float saveMovementLastTime = 0;
148 DebugSessionMovement debugMovement;
149 GameStats origStats; // for replaying
150 GameConfig origConfig; // for replaying
151 int origRoomSeed, origOtherSeed;
157 transient int maskSX, maskSY;
158 transient SpriteImage smask;
159 transient int maskFrame;
163 // ////////////////////////////////////////////////////////////////////////// //
164 final void saveKeyboardBindings () {
165 auto tok = SpawnObject(TempOptionsKeys);
166 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
167 appSaveOptions(tok, "keybindings");
172 final void loadKeyboardBindings () {
173 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
175 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
181 // ////////////////////////////////////////////////////////////////////////// //
182 void saveGameOptions () {
183 appSaveOptions(global.config, "config");
187 void loadGameOptions () {
188 auto cfg = appLoadOptions(GameConfig, "config");
190 auto oldHero = config.heroType;
191 auto tok = SpawnObject(TempOptionsKeys);
192 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
193 delete global.config;
196 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
198 writeln("config loaded");
199 global.restartMusic();
201 //config.heroType = GameConfig::Hero.Spelunker;
202 config.heroType = oldHero;
205 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
209 // ////////////////////////////////////////////////////////////////////////// //
210 void saveGameStats () {
211 if (level.stats) appSaveOptions(level.stats, "stats");
215 void loadGameStats () {
216 auto stats = appLoadOptions(GameStats, "stats");
221 if (!level.stats) level.stats = SpawnObject(GameStats);
222 level.stats.global = global;
226 // ////////////////////////////////////////////////////////////////////////// //
227 struct UIPaneSaveInfo {
229 UIPane::SaveInfo nfo;
232 transient UIPane optionsPane; // either options, or binding editor
234 transient GameLevel::IVec2D optionsPaneOfs;
235 transient void delegate () saveOptionsDG;
237 transient array!UIPaneSaveInfo optionsPaneState;
240 final void saveCurrentPane () {
241 if (!optionsPane || !optionsPane.id) return;
244 if (optionsPane.id == 'CheatFlags') {
245 if (instantGhost && level.ghostTimeLeft > 0) {
246 level.ghostTimeLeft = 1;
250 foreach (ref auto psv; optionsPaneState) {
251 if (psv.id == optionsPane.id) {
252 optionsPane.saveState(psv.nfo);
257 optionsPaneState.length += 1;
258 optionsPaneState[$-1].id = optionsPane.id;
259 optionsPane.saveState(optionsPaneState[$-1].nfo);
263 final void restoreCurrentPane () {
264 if (optionsPane) optionsPane.setupHotkeys(); // why not?
265 if (!optionsPane || !optionsPane.id) return;
266 foreach (ref auto psv; optionsPaneState) {
267 if (psv.id == optionsPane.id) {
268 optionsPane.restoreState(psv.nfo);
275 // ////////////////////////////////////////////////////////////////////////// //
276 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
277 if (!it.tagClass) return;
278 if (class!MapObject(it.tagClass)) {
279 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
280 it.owner.closeMe = true;
285 // ////////////////////////////////////////////////////////////////////////// //
286 transient array!(class!MapObject) cheatItemsList;
289 final void fillCheatItemsList () {
290 cheatItemsList.length = 0;
291 cheatItemsList[$] = ItemProjectileArrow;
292 cheatItemsList[$] = ItemWeaponShotgun;
293 cheatItemsList[$] = ItemWeaponAshShotgun;
294 cheatItemsList[$] = ItemWeaponPistol;
295 cheatItemsList[$] = ItemWeaponMattock;
296 cheatItemsList[$] = ItemWeaponMachete;
297 cheatItemsList[$] = ItemWeaponWebCannon;
298 cheatItemsList[$] = ItemWeaponSceptre;
299 cheatItemsList[$] = ItemWeaponBow;
300 cheatItemsList[$] = ItemBones;
301 cheatItemsList[$] = ItemFakeBones;
302 cheatItemsList[$] = ItemFishBone;
303 cheatItemsList[$] = ItemRock;
304 cheatItemsList[$] = ItemJar;
305 cheatItemsList[$] = ItemSkull;
306 cheatItemsList[$] = ItemGoldenKey;
307 cheatItemsList[$] = ItemGoldIdol;
308 cheatItemsList[$] = ItemCrystalSkull;
309 cheatItemsList[$] = ItemShellSingle;
310 cheatItemsList[$] = ItemChest;
311 cheatItemsList[$] = ItemCrate;
312 cheatItemsList[$] = ItemLockedChest;
313 cheatItemsList[$] = ItemDice;
317 final UIPane createCheatItemsPane () {
318 if (!level.player) return none;
320 UIPane pane = SpawnObject(UIPane);
322 pane.sprStore = sprStore;
324 pane.width = 320*3-64;
325 pane.height = 240*3-64;
327 foreach (auto ipk; cheatItemsList) {
328 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
332 //optionsPaneOfs.x = 100;
333 //optionsPaneOfs.y = 50;
339 // ////////////////////////////////////////////////////////////////////////// //
340 transient array!(class!MapObject) cheatEnemiesList;
343 final void fillCheatEnemiesList () {
344 cheatEnemiesList.length = 0;
345 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
346 cheatEnemiesList[$] = EnemyBat;
347 cheatEnemiesList[$] = EnemySpiderHang;
348 cheatEnemiesList[$] = EnemySpider;
349 cheatEnemiesList[$] = EnemySnake;
350 cheatEnemiesList[$] = EnemyCaveman;
351 cheatEnemiesList[$] = EnemySkeleton;
352 cheatEnemiesList[$] = MonsterShopkeeper;
353 cheatEnemiesList[$] = EnemyZombie;
354 cheatEnemiesList[$] = EnemyVampire;
355 cheatEnemiesList[$] = EnemyFrog;
356 cheatEnemiesList[$] = EnemyGreenFrog;
357 cheatEnemiesList[$] = EnemyFireFrog;
358 cheatEnemiesList[$] = EnemyMantrap;
359 cheatEnemiesList[$] = EnemyScarab;
360 cheatEnemiesList[$] = EnemyFloater;
361 cheatEnemiesList[$] = EnemyBlob;
362 cheatEnemiesList[$] = EnemyMonkey;
363 cheatEnemiesList[$] = EnemyGoldMonkey;
364 cheatEnemiesList[$] = EnemyAlien;
365 cheatEnemiesList[$] = EnemyYeti;
366 cheatEnemiesList[$] = EnemyHawkman;
367 cheatEnemiesList[$] = EnemyUFO;
368 cheatEnemiesList[$] = EnemyYetiKing;
372 final UIPane createCheatEnemiesPane () {
373 if (!level.player) return none;
375 UIPane pane = SpawnObject(UIPane);
377 pane.sprStore = sprStore;
379 pane.width = 320*3-64;
380 pane.height = 240*3-64;
382 foreach (auto ipk; cheatEnemiesList) {
383 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
387 //optionsPaneOfs.x = 100;
388 //optionsPaneOfs.y = 50;
394 // ////////////////////////////////////////////////////////////////////////// //
395 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
398 final void fillCheatPickupList () {
399 cheatPickupList.length = 0;
400 cheatPickupList[$] = ItemPickupBombBag;
401 cheatPickupList[$] = ItemPickupBombBox;
402 cheatPickupList[$] = ItemPickupPaste;
403 cheatPickupList[$] = ItemPickupRopePile;
404 cheatPickupList[$] = ItemPickupShellBox;
405 cheatPickupList[$] = ItemPickupAnkh;
406 cheatPickupList[$] = ItemPickupCape;
407 cheatPickupList[$] = ItemPickupJetpack;
408 cheatPickupList[$] = ItemPickupUdjatEye;
409 cheatPickupList[$] = ItemPickupCrown;
410 cheatPickupList[$] = ItemPickupKapala;
411 cheatPickupList[$] = ItemPickupParachute;
412 cheatPickupList[$] = ItemPickupCompass;
413 cheatPickupList[$] = ItemPickupSpectacles;
414 cheatPickupList[$] = ItemPickupGloves;
415 cheatPickupList[$] = ItemPickupMitt;
416 cheatPickupList[$] = ItemPickupJordans;
417 cheatPickupList[$] = ItemPickupSpringShoes;
418 cheatPickupList[$] = ItemPickupSpikeShoes;
419 cheatPickupList[$] = ItemPickupTeleporter;
423 final UIPane createCheatPickupsPane () {
424 if (!level.player) return none;
426 UIPane pane = SpawnObject(UIPane);
428 pane.sprStore = sprStore;
430 pane.width = 320*3-64;
431 pane.height = 240*3-64;
433 foreach (auto ipk; cheatPickupList) {
434 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
438 //optionsPaneOfs.x = 100;
439 //optionsPaneOfs.y = 50;
445 // ////////////////////////////////////////////////////////////////////////// //
446 transient int instantGhost;
448 final UIPane createCheatFlagsPane () {
449 UIPane pane = SpawnObject(UIPane);
450 pane.id = 'CheatFlags';
451 pane.sprStore = sprStore;
453 pane.width = 320*3-64;
454 pane.height = 240*3-64;
458 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
459 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
460 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
461 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
462 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
463 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
464 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
465 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
466 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
467 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
468 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
469 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
470 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
471 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
472 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
473 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
474 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
475 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
477 optionsPaneOfs.x = 100;
478 optionsPaneOfs.y = 50;
484 final UIPane createOptionsPane () {
485 UIPane pane = SpawnObject(UIPane);
487 pane.sprStore = sprStore;
489 pane.width = 320*3-64;
490 pane.height = 240*3-64;
494 //!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.");
497 UILabel.Create(pane, "VISUAL OPTIONS");
498 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
499 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).");
500 // we don't have intro yet
501 //UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
502 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
505 UILabel.Create(pane, "");
506 UILabel.Create(pane, "HUD OPTIONS");
507 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
508 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.");
509 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
512 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
516 UILabel.Create(pane, "");
517 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
518 //!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.");
519 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
520 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
521 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.");
522 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.");
523 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
524 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.");
525 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
528 UILabel.Create(pane, "");
529 UILabel.Create(pane, "GAMEPLAY OPTIONS");
530 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.");
531 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
532 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!");
533 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.");
534 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.");
535 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
536 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.");
537 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.");
538 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.");
539 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?");
540 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.");
541 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.");
542 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.");
543 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
544 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
545 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
546 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
547 rstl.names[$] = "RANDOM";
548 rstl.names[$] = "NORMAL";
549 rstl.names[$] = "BIZARRE";
552 UILabel.Create(pane, "");
553 UILabel.Create(pane, "WHIP OPTIONS");
554 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
555 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
556 whiptype.names[$] = "NORMAL";
557 whiptype.names[$] = "LONG";
560 UILabel.Create(pane, "");
561 UILabel.Create(pane, "PLAYER OPTIONS");
562 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
563 herotype.names[$] = "SPELUNKY GUY";
564 herotype.names[$] = "DAMSEL";
565 herotype.names[$] = "TUNNEL MAN";
568 UILabel.Create(pane, "");
569 UILabel.Create(pane, "CHEAT OPTIONS");
570 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.");
571 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
572 plrlit.names[$] = "NEVER";
573 plrlit.names[$] = "FORCED DARKNESS";
574 plrlit.names[$] = "ALWAYS";
575 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'.");
576 rdark.names[$] = "NEVER";
577 rdark.names[$] = "DEFAULT";
578 rdark.names[$] = "ALWAYS";
579 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.");
581 rghost.getNameCB = delegate string (int val) {
582 if (val < 0) return "INSTANT";
583 if (val == 0) return "NEVER";
584 if (val < 120) return va("%d SEC", val);
585 if (val%60 == 0) return va("%d MIN", val/60);
586 if (val%60 == 30) return va("%d.5 MIN", val/60);
587 return va("%d MIN, %d SEC", val/60, val%60);
589 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.");
591 UILabel.Create(pane, "");
592 UILabel.Create(pane, "CHEAT START OPTIONS");
593 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.");
594 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
595 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
596 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
597 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
600 UILabel.Create(pane, "");
601 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
602 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
603 mm.names[$] = "SILENCE";
604 mm.names[$] = "RESTART";
605 mm.names[$] = "DON'T TOUCH";
607 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
608 //mm.names[$] = "SILENCE";
609 mm.names[$] = "RESTART";
610 mm.names[$] = "DON'T TOUCH";
613 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
615 swstereo.onValueChanged = delegate void (int newval) {
616 SoundSystem.SwapStereo = newval;
620 UILabel.Create(pane, "");
621 UILabel.Create(pane, "SOUND CONTROL CENTER");
622 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
623 rmusonoff.onValueChanged = delegate void (int newval) {
624 global.restartMusic();
627 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
629 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
630 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
632 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
633 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
636 saveOptionsDG = delegate void () {
637 writeln("saving options");
640 optionsPaneOfs.x = 42;
641 optionsPaneOfs.y = 0;
647 final void createBindingsControl (UIPane pane, int keyidx) {
650 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
651 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
652 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
653 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
654 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
655 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
656 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
657 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
658 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
659 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
660 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
663 int arridx = GameConfig.getKeyIndex(keyidx);
664 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
668 final UIPane createBindingsPane () {
669 UIPane pane = SpawnObject(UIPane);
670 pane.id = 'KeyBindings';
671 pane.sprStore = sprStore;
673 pane.width = 320*3-64;
674 pane.height = 240*3-64;
676 createBindingsControl(pane, GameConfig::Key.Left);
677 createBindingsControl(pane, GameConfig::Key.Right);
678 createBindingsControl(pane, GameConfig::Key.Up);
679 createBindingsControl(pane, GameConfig::Key.Down);
680 createBindingsControl(pane, GameConfig::Key.Jump);
681 createBindingsControl(pane, GameConfig::Key.Run);
682 createBindingsControl(pane, GameConfig::Key.Attack);
683 createBindingsControl(pane, GameConfig::Key.Switch);
684 createBindingsControl(pane, GameConfig::Key.Pay);
685 createBindingsControl(pane, GameConfig::Key.Bomb);
686 createBindingsControl(pane, GameConfig::Key.Rope);
688 saveOptionsDG = delegate void () {
689 writeln("saving keys");
690 saveKeyboardBindings();
692 optionsPaneOfs.x = 120;
693 optionsPaneOfs.y = 140;
699 // ////////////////////////////////////////////////////////////////////////// //
700 void clearGameMovement () {
701 debugMovement = SpawnObject(DebugSessionMovement);
702 debugMovement.playconfig = SpawnObject(GameConfig);
703 debugMovement.playconfig.copyGameplayConfigFrom(config);
704 debugMovement.resetReplay();
708 void saveGameMovement (string fname) {
709 if (debugMovement) appSaveOptions(debugMovement, fname);
710 saveMovementLastTime = GetTickCount();
714 void loadGameMovement (string fname) {
715 delete debugMovement;
716 debugMovement = appLoadOptions(DebugSessionMovement, fname);
717 debugMovement.resetReplay();
720 origStats = level.stats;
721 origStats.global = none;
722 level.stats = SpawnObject(GameStats);
723 level.stats.global = global;
726 config = debugMovement.playconfig;
727 global.config = config;
728 origRoomSeed = global.globalRoomSeed;
729 origOtherSeed = global.globalOtherSeed;
730 writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
735 void stopReplaying () {
737 writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
738 global.globalRoomSeed = origRoomSeed;
739 global.globalOtherSeed = origOtherSeed;
741 delete debugMovement;
742 saveGameSession = false;
743 replayGameSession = false;
744 doGameSavingPlaying = Replay.None;
747 origStats.global = global;
748 level.stats = origStats;
754 global.config = origConfig;
760 // ////////////////////////////////////////////////////////////////////////// //
761 final bool saveGame (string gmname) {
762 return appSaveOptions(level, gmname);
766 final bool loadGame (string gmname) {
767 auto olddel = ImmediateDelete;
768 ImmediateDelete = false;
770 auto stats = level.stats;
773 auto lvl = appLoadOptions(GameLevel, gmname);
775 //lvl.global.config = config;
780 global = level.global;
781 global.config = config;
783 level.sprStore = sprStore;
784 level.bgtileStore = bgtileStore;
787 level.onBeforeFrame = &beforeNewFrame;
788 level.onAfterFrame = &afterNewFrame;
789 level.onInterFrame = &interFrame;
790 level.onLevelExitedCB = &levelExited;
791 level.onCameraTeleported = &cameraTeleportedCB;
793 level.viewWidth = Video.screenWidth;
794 level.viewHeight = Video.screenHeight;
797 level.centerViewAtPlayer();
798 teleportCameraAt(level.viewStart);
800 recalcCameraCoords(0);
805 level.stats.global = level.global;
807 ImmediateDelete = olddel;
808 CollectGarbage(true); // destroy delayed objects too
813 // ////////////////////////////////////////////////////////////////////////// //
814 float lastThinkerTime;
815 int replaySkipFrame = 0;
818 final void onTimePasses () {
819 float curTime = GetTickCount();
820 if (lastThinkerTime > 0) {
821 if (curTime < lastThinkerTime) {
822 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
823 lastThinkerTime = curTime;
826 if (replayFastForward && replaySkipFrame) {
828 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
831 level.processThinkers(curTime-lastThinkerTime);
833 lastThinkerTime = curTime;
837 final void resetFramesAndForceOne () {
838 float curTime = GetTickCount();
839 lastThinkerTime = curTime;
841 auto wasPaused = level.gamePaused;
842 level.gamePaused = false;
843 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
844 level.processThinkers(GameLevel::FrameTime);
845 level.gamePaused = wasPaused;
846 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
850 // ////////////////////////////////////////////////////////////////////////// //
851 private float currFrameDelta; // so level renderer can properly interpolate the player
852 private GameLevel::IVec2D camPrev, camCurr;
853 private GameLevel::IVec2D camShake;
854 private GameLevel::IVec2D viewCameraPos;
857 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
862 viewCameraPos.x = pos.x;
863 viewCameraPos.y = pos.y;
869 // call `recalcCameraCoords()` to get real camera coords after this
870 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
871 // check if camera is moved too far, and teleport it
873 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
874 abs(camCurr.y-pos.y)/global.scale >= 16*4))
876 teleportCameraAt(pos);
878 camPrev.x = camCurr.x;
879 camPrev.y = camCurr.y;
883 camShake.x = level.shakeDir.x*global.scale;
884 camShake.y = level.shakeDir.y*global.scale;
888 final void recalcCameraCoords (float frameDelta) {
889 currFrameDelta = frameDelta;
890 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
891 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
893 // update sound listener position (it is either at player position, or in viewport center)
897 (viewCameraPos.x+level.viewWidth/2.0)/global.scale,
898 (viewCameraPos.y+level.viewHeight/2.0)/global.scale
901 viewCameraPos.x += camShake.x;
902 viewCameraPos.y += camShake.y;
903 lv = vector(float(level.player.xCenter), float(level.player.yCenter));
905 SoundSystem.ListenerOrigin = lv;
906 SoundSystem.UpdateSounds();
910 GameLevel::SavedKeyState savedKeyState;
912 final void pauseGame () {
913 if (!level.gamePaused) {
914 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
915 level.gamePaused = true;
920 final void unpauseGame () {
921 if (level.gamePaused) {
922 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
923 level.gamePaused = false;
924 //lastThinkerTime = 0;
930 final void beforeNewFrame (bool frameSkip) {
932 level.disablePlayerThink = true;
935 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
936 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
937 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
939 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
940 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
941 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
942 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
944 level.disablePlayerThink = false;
948 if (level.isKeyDown(PlayerPawn::KeyLeft)) level.player.fltx -= delta;
949 if (level.isKeyDown(PlayerPawn::KeyRight)) level.player.fltx += delta;
950 if (level.isKeyDown(PlayerPawn::KeyUp)) level.player.flty -= delta;
951 if (level.isKeyDown(PlayerPawn::KeyDown)) level.player.flty += delta;
954 if (!level.gamePaused) {
955 // save seeds for afterframe processing
957 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
958 debugMovement.otherSeed = global.globalOtherSeed;
959 debugMovement.roomSeed = global.globalRoomSeed;
963 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
965 #ifdef BIGGER_REPLAY_DATA
966 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
967 debugMovement.keypresses.length += 1;
968 level.keysSaveState(debugMovement.keypresses[$-1]);
969 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
970 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
974 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
975 #ifdef BIGGER_REPLAY_DATA
976 if (debugMovement.keypos < debugMovement.keypresses.length) {
977 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
978 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
979 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
980 ++debugMovement.keypos;
986 auto code = debugMovement.getKey(out kbidx, out down);
987 if (code == DebugSessionMovement::END_OF_RECORD) {
988 // do this in main loop, so we can view totals
992 if (code == DebugSessionMovement::END_OF_FRAME) {
995 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
996 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1004 final void afterNewFrame (bool frameSkip) {
1005 if (!replayFastForward) replaySkipFrame = 0;
1007 if (level.gamePaused) return;
1009 if (!level.gamePaused) {
1010 if (doGameSavingPlaying != Replay.None) {
1011 if (doGameSavingPlaying == Replay.Saving) {
1012 replayFastForward = false; // just in case
1013 #ifndef BIGGER_REPLAY_DATA
1014 debugMovement.addEndOfFrame();
1016 auto stt = GetTickCount();
1017 if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
1018 } else if (doGameSavingPlaying == Replay.Replaying) {
1019 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1020 replaySkipFrame = 1;
1026 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1027 //SoundSystem.UpdateSounds();
1029 if (!freeRide) level.fixCamera();
1030 setNewCameraPos(level.viewStart);
1032 prevCameraX = currCameraX;
1033 prevCameraY = currCameraY;
1034 currCameraX = level.cameraX;
1035 currCameraY = level.cameraY;
1036 // disable camera interpolation if the screen is shaking
1037 if (level.shakeX|level.shakeY) {
1038 prevCameraX = currCameraX;
1039 prevCameraY = currCameraY;
1042 // disable camera interpolation if it moves too far away
1043 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1044 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1046 if (switchInterpolator) {
1047 switchInterpolator = false;
1048 config.interpolateMovement = !config.interpolateMovement;
1050 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0); // recalc camera coords
1052 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1053 pauseRequested = false;
1055 if (!showHelp) showHelp = true;
1061 final void interFrame (float frameDelta) {
1062 if (!config.interpolateMovement) return;
1063 recalcCameraCoords(frameDelta);
1067 final void cameraTeleportedCB () {
1068 teleportCameraAt(level.viewStart);
1069 recalcCameraCoords(0);
1073 // ////////////////////////////////////////////////////////////////////////// //
1075 final void setColorByIdx (bool isset, int col) {
1077 // missed collision: red
1078 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1079 } else if (col == -999) {
1080 // superfluous collision: blue
1081 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1082 } else if (col <= 0) {
1083 // no collision: yellow
1084 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1085 } else if (col > 0) {
1087 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1092 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1094 CollisionMask cm = CollisionMask.Create(frm, false);
1096 int scale = global.config.scale;
1097 int bx0, by0, bx1, by1;
1098 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1099 Video.color = 0x7f_00_00_ff;
1100 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1101 if (!cm.isEmptyMask) {
1102 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1103 foreach (int iy; 0..cm.height) {
1104 foreach (int ix; 0..cm.width) {
1105 int v = cm.mask[ix, iy];
1106 foreach (int dx; 0..32) {
1109 Video.color = 0x3f_00_ff_00;
1110 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1119 foreach (int iy; 0..frm.tex.height) {
1120 foreach (int ix; 0..(frm.tex.width+31)/31) {
1121 foreach (int dx; 0..32) {
1123 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1124 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1125 setColorByIdx(true, col);
1126 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1128 Video.color = 0xaf_00_ff_00;
1130 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1136 if (frm.bw > 0 && frm.bh > 0) {
1137 setColorByIdx(true, col);
1138 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1139 Video.color = 0xff_00_00;
1140 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1149 // ////////////////////////////////////////////////////////////////////////// //
1150 transient int drawStats;
1151 transient array!int statsTopItem;
1154 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1155 auto sa = string(a.objName);
1156 auto sb = string(b.objName);
1161 final int getStatsTopItem () {
1162 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1166 final void setStatsTopItem (int val) {
1167 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1168 statsTopItem[drawStats] = val;
1172 final void resetStatsTopItem () {
1177 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1178 sprStore.loadFont('sFontSmall');
1184 final int calcStatsVisItems () {
1187 statsDrawGetStartPosLoadFont(currX, currY);
1188 int endY = Video.screenHeight-(currY*2);
1189 return max(1, endY/sprStore.getFontHeight(scale));
1193 int getStatsItemCount () {
1194 switch (drawStats) {
1195 case 2: return level.stats.totalKills.length;
1196 case 3: return level.stats.totalDeaths.length;
1197 case 4: return level.stats.totalCollected.length;
1203 final void statsMoveUp () {
1204 int count = getStatsItemCount();
1205 if (count < 0) return;
1206 int visItems = calcStatsVisItems();
1207 if (count <= visItems) { resetStatsTopItem(); return; }
1208 int top = getStatsTopItem();
1210 setStatsTopItem(top-1);
1214 final void statsMoveDown () {
1215 int count = getStatsItemCount();
1216 if (count < 0) return;
1217 int visItems = calcStatsVisItems();
1218 if (count <= visItems) { resetStatsTopItem(); return; }
1219 int top = getStatsTopItem();
1220 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1221 top = clamp(top+1, 0, count-visItems);
1222 setStatsTopItem(top);
1226 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1227 GameStats.sortTotalsList(arr, &totalsNameCmpCB);
1231 statsDrawGetStartPosLoadFont(currX, currY);
1233 int endY = Video.screenHeight-(currY*2);
1234 int visItems = calcStatsVisItems();
1236 if (arr.length <= visItems) resetStatsTopItem();
1238 int topItem = getStatsTopItem();
1242 Video.color = 0x3f_ff_ff_00;
1243 auto spr = sprStore['sPageUp'];
1244 spr.frames[0].tex.blitAt(currX-24, currY, scale);
1247 // "downscroll" mark
1248 if (topItem+visItems < arr.length) {
1249 Video.color = 0x3f_ff_ff_00;
1250 auto spr = sprStore['sPageDown'];
1251 spr.frames[0].tex.blitAt(currX-24, endY/*-sprStore.getFontHeight(scale)*/, scale);
1254 Video.color = 0xff_ff_00;
1255 int hiColor = 0x00_ff_00;
1256 int hiColor1 = 0xf_ff_ff;
1259 while (it < arr.length && visItems-- > 0) {
1260 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);
1261 currY += sprStore.getFontHeight(scale);
1267 void drawStatsScreen () {
1268 int deathCount, killCount, collectCount;
1270 switch (drawStats) {
1271 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1272 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1273 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1275 if (drawStats > 1) {
1277 foreach (ref auto i; statsTopItem) i = 0;
1282 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1283 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1284 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1286 Video.color = 0xff_ff_00;
1287 int hiColor = 0x00_ff_00;
1288 sprStore.loadFont('sFontSmall');
1294 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1295 currY += sprStore.getFontHeight(scale);
1297 int gw = level.stats.gamesWon;
1298 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1299 currY += sprStore.getFontHeight(scale);
1301 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1302 currY += sprStore.getFontHeight(scale);
1304 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1305 currY += sprStore.getFontHeight(scale);
1307 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1308 currY += sprStore.getFontHeight(scale);
1310 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1311 currY += sprStore.getFontHeight(scale);
1313 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1314 currY += sprStore.getFontHeight(scale);
1316 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1317 currY += sprStore.getFontHeight(scale);
1319 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1320 currY += sprStore.getFontHeight(scale);
1322 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1323 currY += sprStore.getFontHeight(scale);
1325 int gs = level.stats.totalGhostSummoned;
1326 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1327 currY += sprStore.getFontHeight(scale);
1329 currY += sprStore.getFontHeight(scale);
1330 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1331 currY += sprStore.getFontHeight(scale);
1336 if (Video.frameTime == 0) {
1338 Video.requestRefresh();
1343 if (level.framesProcessedFromLastClear < 1) return;
1344 calcMouseMapCoords();
1346 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1347 Video.clearScreen();
1348 Video.stencil = false;
1349 Video.color = 0xff_ff_ff;
1350 Video.textureFiltering = false;
1351 // don't touch framebuffer alpha
1352 Video.colorMask = Video::CMask.Colors;
1354 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1356 if (level.gamePaused) {
1357 if (mouseLevelX != int.min) {
1358 int scale = level.global.scale;
1359 if (renderMouseRect) {
1360 Video.color = 0xcf_ff_ff_00;
1361 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1363 if (renderMouseTile) {
1364 Video.color = 0xaf_ff_00_00;
1365 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1370 switch (doGameSavingPlaying) {
1372 Video.color = 0x7f_00_ff_00;
1373 sprStore.loadFont('sFont');
1374 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1376 case Replay.Replaying:
1377 if (level.player && !level.player.dead) {
1378 Video.color = 0x7f_ff_00_00;
1379 sprStore.loadFont('sFont');
1380 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1381 int th = sprStore.getFontHeight(2);
1382 if (replayFastForward) {
1383 sprStore.loadFont('sFontSmall');
1384 string sstr = va("x%d", replayFastForwardSpeed+1);
1385 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1390 if (saveGameSession) {
1391 Video.color = 0x7f_ff_7f_00;
1392 sprStore.loadFont('sFont');
1393 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1399 if (level.player && level.player.dead && !showHelp) {
1401 Video.color = 0x8f_00_00_00;
1402 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1407 if (true /*level.inWinCutscene == 0*/) {
1408 Video.color = 0xff_ff_ff;
1409 sprStore.loadFont('sFontSmall');
1410 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1412 "PRESS $PAY TO RESTART GAME\n"~
1414 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1416 "TOTAL PLAYING TIME: |%s|"~
1418 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1419 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1420 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1422 GameLevel.time2str(level.stats.playingTime)
1424 kmsg = global.expandString(kmsg);
1425 sprStore.renderMultilineTextCentered(Video.screenWidth/2, int.min, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1432 Video.color = 0xff_7f_00;
1433 sprStore.loadFont('sFontSmall');
1434 sprStore.renderText(8, Video.screenHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1435 auto spf = smask.frames[maskFrame];
1436 sprStore.renderText(8, Video.screenHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1438 spf.bx, spf.by, spf.bw, spf.bh,
1439 (spf.maskEmpty ? "TAN" : "ONA"),
1440 (spf.precise ? "TAN" : "ONA")),
1443 //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1444 //writeln("pos=(", maskSX, ",", maskSY, ")");
1445 int scale = global.config.scale;
1446 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1447 int mapX = xofs/scale+maskSX;
1448 int mapY = yofs/scale+maskSY;
1451 writeln("==== tiles ====");
1453 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1454 if (t.spectral || !t.isInstanceAlive) return false;
1455 Video.color = 0x7f_ff_00_00;
1456 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);
1457 auto tsf = t.getSpriteFrame();
1459 auto spf = smask.frames[maskFrame];
1460 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1461 int mapX = xofs/global.config.scale+maskSX;
1462 int mapY = yofs/global.config.scale+maskSY;
1465 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1466 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1467 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1471 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1472 Video.color = 0x7f_ff_00_00;
1473 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);
1477 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1479 Video.color = 0xaf_ff_ff_ff;
1480 spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1481 Video.color = 0xff_ff_00;
1482 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1486 int fx0, fy0, fx1, fy1;
1487 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1488 Video.color = 0x7f_00_00_ff;
1489 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1495 Video.color = 0x8f_00_00_00;
1496 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1498 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1503 Video.color = 0xff_ff_00;
1504 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1505 if (showHelp == 1) {
1506 sprStore.loadFont('sFontSmall');
1507 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1508 "F1: show this help\n"~
1510 "K : redefine keys\n"~
1511 "I : toggle interpolaion\n"~
1512 "N : create some blood\n"~
1513 "R : generate a new level\n"~
1514 "F : toggle \"Frozen Area\"\n"~
1515 "X : resurrect player\n"~
1516 "Q : teleport to exit\n"~
1517 "D : teleport to damel\n"~
1519 "C : cheat flags menu\n"~
1520 "P : cheat pickup menu\n"~
1521 "E : cheat enemy menu\n"~
1522 "Enter: cheat items menu\n"~
1524 "TAB: toggle 'freeroam' mode\n"~
1530 //SoundSystem.UpdateSounds();
1532 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1535 Video.color = 0xaf_ff_ff_ff;
1536 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1541 // ////////////////////////////////////////////////////////////////////////// //
1542 transient bool gameJustOver;
1543 transient bool waitingForPayRestart;
1546 final void calcMouseMapCoords () {
1547 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1548 mouseLevelX = int.min;
1549 mouseLevelY = int.min;
1552 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1553 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1554 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1558 final void onEvent (ref event_t evt) {
1559 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1561 if (evt.type == ev_winfocus) {
1562 if (level && !evt.focused) {
1567 //writeln("FOCUS!");
1568 Video.getMousePos(out mouseX, out mouseY);
1573 if (evt.type == ev_mouse) {
1576 calcMouseMapCoords();
1579 if (evt.type == ev_keydown) {
1580 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1581 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1582 renderMouseTile = evt.bCtrl;
1583 renderMouseRect = evt.bAlt;
1586 if (evt.type == ev_keyup) {
1587 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1588 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1589 renderMouseTile = evt.bCtrl;
1590 renderMouseRect = evt.bAlt;
1593 if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1595 if (evt.type == ev_keydown && evt.bAlt && (evt.keycode >= "1" && evt.keycode <= "4")) {
1596 int newScale = evt.keycode-48;
1597 if (global.config.scale != newScale) {
1598 global.config.scale = newScale;
1601 cameraTeleportedCB();
1608 if (evt.type == ev_mouse) {
1609 maskSX = evt.x/global.config.scale;
1610 maskSY = evt.y/global.config.scale;
1613 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1614 maskFrame = max(0, maskFrame-1);
1617 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1618 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1627 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1629 if (saveOptionsDG) saveOptionsDG();
1630 saveOptionsDG = none;
1632 SoundSystem.UpdateSounds(); // just in case
1633 if (global.hasSpectacles) level.pickedSpectacles();
1636 optionsPane.onEvent(evt);
1640 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1641 if (evt.type == ev_keydown) {
1642 switch (evt.keycode) {
1643 case K_F1: if (showHelp > 1) showHelp = 1; else unpauseGame(); return;
1644 case K_F10: Video.requestQuit(); return;
1645 case K_F12: showHelp = 3-showHelp; return;
1647 case K_UPARROW: case K_PAD8:
1648 if (drawStats) statsMoveUp();
1650 case K_DOWNARROW: case K_PAD2:
1651 if (drawStats) statsMoveDown();
1664 resetFramesAndForceOne();
1678 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1679 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1680 case K_c: optionsPane = createCheatFlagsPane(); restoreCurrentPane(); return;
1681 case K_p: optionsPane = createCheatPickupsPane(); restoreCurrentPane(); return;
1682 case K_ENTER: optionsPane = createCheatItemsPane(); restoreCurrentPane(); return;
1683 case K_e: optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); return;
1684 case K_TAB: freeRide = !freeRide; return;
1685 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1686 //case K_j: global.hasJordans = !global.hasJordans; return;
1687 case K_i: switchInterpolator = true; unpauseGame(); return;
1691 auto bomb = ItemBomb(level.MakeMapObject(level.player.ix, level.player.iy, 'oBomb'));
1692 if (bomb) bomb.armIt();
1694 level.resurrectPlayer();
1699 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1700 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1701 if (evt.bAlt && level.player && level.player.dead) {
1702 saveGameSession = false;
1703 replayGameSession = true;
1707 if (evt.bCtrl) global.idol = false;
1708 level.generateLevel();
1709 level.centerViewAtPlayer();
1710 teleportCameraAt(level.viewStart);
1711 resetFramesAndForceOne();
1714 global.toggleMusic();
1717 level.pickedSpectacles();
1720 global.config.useFrozenRegion = !global.config.useFrozenRegion;
1724 if (level.allExits.length) {
1725 level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1731 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1733 level.teleportPlayerTo(damsel.ix, damsel.iy);
1740 auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1742 level.teleportPlayerTo(obj.ix, obj.iy-4);
1749 auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1751 level.teleportPlayerTo(obj.ix, obj.iy);
1758 if (level && mouseLevelX != int.min) {
1759 int scale = level.global.scale;
1760 int mapX = mouseLevelX;
1761 int mapY = mouseLevelY;
1762 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1767 if (level && mouseLevelX != int.min) {
1768 int scale = level.global.scale;
1769 int mapX = mouseLevelX;
1770 int mapY = mouseLevelY;
1773 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1774 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1775 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1779 foreach (MapTile t; level.miscTileGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false)) {
1780 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1786 auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1787 //if (obj) obj.monkey = monkey;
1789 //playSound('sndThump');
1795 case K_DELETE: // suicide
1796 if (doGameSavingPlaying == Replay.None) {
1797 if (level.player && !level.player.dead && evt.bCtrl) {
1798 global.hasAnkh = false;
1799 level.global.plife = 1;
1800 level.player.invincible = 0;
1801 level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion');
1808 if (level.player && !level.player.dead && evt.bAlt) {
1809 if (doGameSavingPlaying != Replay.None) {
1810 if (doGameSavingPlaying == Replay.Replaying) {
1812 } else if (doGameSavingPlaying == Replay.Saving) {
1813 saveGameMovement(dbgSessionMovementFileName);
1815 doGameSavingPlaying = Replay.None;
1817 saveGameSession = false;
1818 replayGameSession = false;
1825 level.stats.setMoneyCheat();
1826 level.stats.addMoney(10000);
1831 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1832 if (level.player && level.player.dead) {
1833 //Video.requestQuit();
1835 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1837 #ifdef QUIT_DOUBLE_ESC
1838 if (++escCount == 2) Video.requestQuit();
1841 pauseRequested = true;
1846 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; return; }
1849 if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1852 if (!level.player || !level.player.dead) {
1853 gameJustOver = false;
1854 } else if (level.player && level.player.dead) {
1855 if (!gameJustOver) {
1857 gameJustOver = true;
1858 waitingForPayRestart = true;
1859 level.clearKeysPressRelease();
1860 if (doGameSavingPlaying == Replay.None) {
1861 stopReplaying(); // just in case
1865 replayFastForward = false;
1866 if (doGameSavingPlaying == Replay.Saving) {
1867 if (debugMovement) saveGameMovement(dbgSessionMovementFileName);
1868 doGameSavingPlaying = Replay.None;
1869 //clearGameMovement();
1870 saveGameSession = false;
1871 replayGameSession = false;
1874 if (evt.type == ev_keydown || evt.type == ev_keyup) {
1875 bool down = (evt.type == ev_keydown);
1876 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
1877 if (down && evt.keycode == K_f) {
1879 if (replayFastForwardSpeed != 4) {
1880 replayFastForwardSpeed = 4;
1881 replayFastForward = true;
1883 replayFastForward = !replayFastForward;
1886 replayFastForwardSpeed = 2;
1887 replayFastForward = !replayFastForward;
1891 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
1892 foreach (int kbidx, int kval; global.config.keybinds) {
1893 if (kval && kval == evt.keycode) {
1894 #ifndef BIGGER_REPLAY_DATA
1895 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
1897 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1901 if (level.player && level.player.dead) {
1902 if (down && evt.keycode == K_r && evt.bAlt) {
1903 saveGameSession = false;
1904 replayGameSession = true;
1907 if (down && evt.keycode == K_s && evt.bAlt) {
1908 bool wasSaveReq = saveGameSession;
1909 stopReplaying(); // just in case
1910 saveGameSession = !wasSaveReq;
1911 replayGameSession = false;
1914 if (replayGameSession) {
1915 stopReplaying(); // just in case
1916 saveGameSession = false;
1917 replayGameSession = false;
1918 loadGameMovement(dbgSessionMovementFileName);
1919 loadGame(dbgSessionStateFileName);
1920 doGameSavingPlaying = Replay.Replaying;
1922 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
1923 if (waitingForPayRestart) {
1924 level.isKeyReleased(GameConfig::Key.Pay);
1925 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
1927 level.isKeyPressed(GameConfig::Key.Pay);
1928 if (level.isKeyReleased(GameConfig::Key.Pay)) {
1929 auto doSave = saveGameSession;
1930 stopReplaying(); // just in case
1931 level.clearKeysPressRelease();
1932 level.restartGame();
1933 level.generateNormalLevel();
1935 saveGameSession = false;
1936 replayGameSession = false;
1937 writeln("DBG: saving game session...");
1938 clearGameMovement();
1939 doGameSavingPlaying = Replay.Saving;
1940 saveGame(dbgSessionStateFileName);
1941 saveGameMovement(dbgSessionMovementFileName);
1952 void levelExited () {
1958 final void runGameLoop () {
1959 Video.frameTime = 0; // unlimited FPS
1960 lastThinkerTime = 0;
1962 sprStore = SpawnObject(SpriteStore);
1963 sprStore.bDumpLoaded = false;
1965 bgtileStore = SpawnObject(BackTileStore);
1966 bgtileStore.bDumpLoaded = false;
1968 level = SpawnObject(GameLevel);
1969 level.setup(global, sprStore, bgtileStore);
1971 level.global = global;
1972 level.sprStore = sprStore;
1973 level.bgtileStore = bgtileStore;
1977 level.onBeforeFrame = &beforeNewFrame;
1978 level.onAfterFrame = &afterNewFrame;
1979 level.onInterFrame = &interFrame;
1980 level.onLevelExitedCB = &levelExited;
1981 level.onCameraTeleported = &cameraTeleportedCB;
1984 maskSX = -0x0ff_fff;
1986 smask = sprStore['sExplosionMask'];
1990 sprStore.loadFont('sFontSmall');
1992 Video.swapInterval = (global.config.optVSync ? 1 : 0);
1993 Video.openScreen("Spelunky/VaVoom C", 320*3, 240*3);
1995 if (Video.realStencilBits < 8) {
1996 Video.closeScreen();
1997 FatalError("FATAL: no stencil buffer!");
1999 if (!Video.framebufferHasAlpha) {
2000 Video.closeScreen();
2001 FatalError("FATAL: no alpha channel in framebuffer!");
2004 //SoundSystem.SwapStereo = config.swapStereo;
2005 SoundSystem.DopplerFactor = 1.0f;
2006 SoundSystem.DopplerVelocity = 10000.0f;
2007 SoundSystem.RolloffFactor = 1.0f;
2008 //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)
2009 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)
2010 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
2011 SoundSystem.NumChannels = 64;
2012 SoundSystem.Sound2DPos = vector(0, 0, -1);
2014 SoundSystem.Initialize();
2015 global.fixVolumes();
2017 level.viewWidth = Video.screenWidth;
2018 level.viewHeight = Video.screenHeight;
2020 level.restartGame(); // this will NOT generate a new level
2025 texTigerEye = GLTexture.Load("sprites/teye0.png");
2027 if (global.cheatEndGameSequence) {
2028 level.winTime = 12*60+42;
2029 level.stats.money = 6666;
2030 switch (global.cheatEndGameSequence) {
2031 case 1: default: level.startWinCutscene(); break;
2032 case 2: level.startWinCutsceneVolcano(); break;
2033 case 3: level.startWinCutsceneWinFall(); break;
2036 switch (startMode) {
2037 case StartMode.Title: level.restartTitle(); break;
2038 case StartMode.Stars: level.restartStarsRoom(); break;
2039 case StartMode.Sun: level.restartSunRoom(); break;
2040 case StartMode.Moon: level.restartMoonRoom(); break;
2042 level.generateNormalLevel();
2043 if (startMode == StartMode.Dead) {
2044 level.player.dead = true;
2045 level.player.visible = false;
2051 //global.rope = 666;
2052 //global.bombs = 666;
2054 //global.globalRoomSeed = 871520037;
2055 //global.globalOtherSeed = 1047036290;
2057 //level.createTitleRoom();
2058 //level.createTrans4Room();
2059 //level.createOlmecRoom();
2060 //level.generateLevel();
2062 //level.centerViewAtPlayer();
2063 teleportCameraAt(level.viewStart);
2064 //writeln(Video.swapInterval);
2066 Video.runEventLoop();
2067 Video.closeScreen();
2069 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName);
2074 level.clearObjects();
2078 // ////////////////////////////////////////////////////////////////////////// //
2079 // duplicates are not allowed!
2080 final void checkGameObjNames () {
2081 array!(class!Object) known;
2083 int classCount = 0, namedCount = 0;
2084 foreach AllClasses(Object, out cc) {
2085 auto gn = GetClassGameObjName(cc);
2087 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2088 auto nid = NameToInt(gn);
2089 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));
2095 writeln(classCount, " classes, ", namedCount, " game object classes.");
2099 // ////////////////////////////////////////////////////////////////////////// //
2100 #include "timelimit.vc"
2101 //const int TimeLimitDate = 2018232;
2104 void performTimeCheck () {
2105 #ifdef DISABLE_TIME_CHECK
2107 if (TigerEye) return;
2110 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2113 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2115 int tldate = tm.year*1000+tm.yday;
2117 if (tldate > TimeLimitDate) {
2118 level.maxPlayingTime = 24;
2120 //writeln("*** days left: ", TimeLimitDate-tldate);
2126 void setupCheats () {
2128 //startMode = StartMode.Dead;
2129 //startMode = StartMode.Title;
2130 //startMode = StartMode.Stars;
2131 //startMode = StartMode.Sun;
2132 startMode = StartMode.Moon;
2134 //global.scumGenSacrificePit = true;
2135 //global.scumAlwaysSacrificeAltar = true;
2137 // first lush jungle level
2138 //global.levelType = 1;
2140 global.scumGenCemetary = true;
2142 //global.idol = false;
2143 //global.currLevel = 5;
2145 //global.isTunnelMan = true;
2148 //global.currLevel = 5;
2149 //global.scumGenLake = true;
2151 //global.currLevel = 5;
2152 //global.currLevel = 9;
2153 //global.currLevel = 13;
2154 //global.currLevel = 14;
2155 //global.cheatEndGameSequence = 1;
2158 //global.currLevel = 6;
2159 global.scumGenAlienCraft = true;
2160 global.currLevel = 9;
2161 //global.scumGenYetiLair = true;
2162 //global.genBlackMarket = true;
2163 //startDead = false;
2164 startMode = StartMode.Alive;
2167 global.cheatCanSkipOlmec = true;
2168 global.currLevel = 15;
2169 startMode = StartMode.Alive;
2172 global.scumGenShop = true;
2173 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2174 global.scumGenShopType = GameGlobal::ShopType.Craps;
2175 //global.scumGenShopType = 6; // craps
2176 //global.scumGenShopType = 7; // kissing
2178 //global.scumAlwaysSacrificeAltar = true;
2182 void setupSeeds () {
2186 // ////////////////////////////////////////////////////////////////////////// //
2188 checkGameObjNames();
2190 appSetName("k8spelunky");
2191 config = SpawnObject(GameConfig);
2192 global = SpawnObject(GameGlobal);
2193 global.config = config;
2194 config.heroType = GameConfig::Hero.Spelunker;
2196 global.randomizeSeedAll();
2198 fillCheatPickupList();
2199 fillCheatItemsList();
2200 fillCheatEnemiesList();
2203 loadKeyboardBindings();