1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
11 * Spelunky is distributed in the hope that it will be entertaining and useful,
12 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
14 * The Spelunky User License should be available in "Game Information", which
15 * can be found in the Resource Explorer, or as an external file called COPYING.
16 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
18 **********************************************************************************/
26 //#define QUIT_DOUBLE_ESC
30 //#define BIGGER_REPLAY_DATA
32 // ////////////////////////////////////////////////////////////////////////// //
33 #include "mapent/0all.vc"
34 #include "PlayerPawn.vc"
35 #include "PlayerPowerup.vc"
36 #include "GameLevel.vc"
39 // ////////////////////////////////////////////////////////////////////////// //
40 #include "uisimple.vc"
43 // ////////////////////////////////////////////////////////////////////////// //
44 class DebugSessionMovement : Object;
46 #ifdef BIGGER_REPLAY_DATA
47 array!(GameLevel::SavedKeyState) keypresses;
49 array!ubyte keypresses; // on each frame
51 GameConfig playconfig;
54 transient int otherSeed, roomSeed;
57 override void Destroy () {
59 keypresses.length = 0;
64 final void resetReplay () {
69 #ifndef BIGGER_REPLAY_DATA
70 final void addKey (int kbidx, bool down) {
71 if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
72 keypresses[$] = kbidx|(down ? 0x80 : 0);
76 final void addEndOfFrame () {
87 final int getKey (out int kbidx, out bool down) {
88 if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
89 if (keypos >= keypresses.length) return END_OF_RECORD;
90 ubyte b = keypresses[keypos++];
91 if (b == 0xff) return END_OF_FRAME;
99 // ////////////////////////////////////////////////////////////////////////// //
100 class TempOptionsKeys : Object;
102 int[16*GameConfig::MaxActionBinds] keybinds;
106 // ////////////////////////////////////////////////////////////////////////// //
109 transient string dbgSessionStateFileName = "debug_game_session_state";
110 transient string dbgSessionMovementFileName = "debug_game_session_movement";
112 GLTexture texTigerEye;
116 SpriteStore sprStore;
117 BackTileStore bgtileStore;
120 int mouseX = int.min, mouseY = int.min;
121 int mouseLevelX = int.min, mouseLevelY = int.min;
122 bool renderMouseTile;
123 bool renderMouseRect;
135 StartMode startMode = StartMode.Intro;
139 bool replayFastForward = false;
140 int replayFastForwardSpeed = 2;
141 bool saveGameSession = false;
142 bool replayGameSession = false;
148 Replay doGameSavingPlaying = Replay.None;
149 float saveMovementLastTime = 0;
150 DebugSessionMovement debugMovement;
151 GameStats origStats; // for replaying
152 GameConfig origConfig; // for replaying
153 int origRoomSeed, origOtherSeed;
161 transient int maskSX, maskSY;
162 transient SpriteImage smask;
163 transient int maskFrame;
167 // ////////////////////////////////////////////////////////////////////////// //
168 final void saveKeyboardBindings () {
169 auto tok = SpawnObject(TempOptionsKeys);
170 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
171 appSaveOptions(tok, "keybindings");
176 final void loadKeyboardBindings () {
177 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
179 if (tok.kbversion != TempOptionsKeys.default.kbversion) {
180 global.config.resetKeybindings();
182 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
189 // ////////////////////////////////////////////////////////////////////////// //
190 void saveGameOptions () {
191 appSaveOptions(global.config, "config");
195 void loadGameOptions () {
196 auto cfg = appLoadOptions(GameConfig, "config");
198 auto oldHero = config.heroType;
199 auto tok = SpawnObject(TempOptionsKeys);
200 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
201 delete global.config;
204 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
206 writeln("config loaded");
207 global.restartMusic();
209 //config.heroType = GameConfig::Hero.Spelunker;
210 config.heroType = oldHero;
213 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
217 // ////////////////////////////////////////////////////////////////////////// //
218 void saveGameStats () {
219 if (level.stats) appSaveOptions(level.stats, "stats");
223 void loadGameStats () {
224 auto stats = appLoadOptions(GameStats, "stats");
229 if (!level.stats) level.stats = SpawnObject(GameStats);
230 level.stats.global = global;
234 // ////////////////////////////////////////////////////////////////////////// //
235 struct UIPaneSaveInfo {
237 UIPane::SaveInfo nfo;
240 transient UIPane optionsPane; // either options, or binding editor
242 transient GameLevel::IVec2D optionsPaneOfs;
243 transient void delegate () saveOptionsDG;
245 transient array!UIPaneSaveInfo optionsPaneState;
248 final void saveCurrentPane () {
249 if (!optionsPane || !optionsPane.id) return;
252 if (optionsPane.id == 'CheatFlags') {
253 if (instantGhost && level.ghostTimeLeft > 0) {
254 level.ghostTimeLeft = 1;
258 foreach (ref auto psv; optionsPaneState) {
259 if (psv.id == optionsPane.id) {
260 optionsPane.saveState(psv.nfo);
265 optionsPaneState.length += 1;
266 optionsPaneState[$-1].id = optionsPane.id;
267 optionsPane.saveState(optionsPaneState[$-1].nfo);
271 final void restoreCurrentPane () {
272 if (optionsPane) optionsPane.setupHotkeys(); // why not?
273 if (!optionsPane || !optionsPane.id) return;
274 foreach (ref auto psv; optionsPaneState) {
275 if (psv.id == optionsPane.id) {
276 optionsPane.restoreState(psv.nfo);
283 // ////////////////////////////////////////////////////////////////////////// //
284 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
285 if (!it.tagClass) return;
286 if (class!MapObject(it.tagClass)) {
287 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
288 it.owner.closeMe = true;
293 // ////////////////////////////////////////////////////////////////////////// //
294 transient array!(class!MapObject) cheatItemsList;
297 final void fillCheatItemsList () {
298 cheatItemsList.length = 0;
299 cheatItemsList[$] = ItemProjectileArrow;
300 cheatItemsList[$] = ItemWeaponShotgun;
301 cheatItemsList[$] = ItemWeaponAshShotgun;
302 cheatItemsList[$] = ItemWeaponPistol;
303 cheatItemsList[$] = ItemWeaponMattock;
304 cheatItemsList[$] = ItemWeaponMachete;
305 cheatItemsList[$] = ItemWeaponWebCannon;
306 cheatItemsList[$] = ItemWeaponSceptre;
307 cheatItemsList[$] = ItemWeaponBow;
308 cheatItemsList[$] = ItemBones;
309 cheatItemsList[$] = ItemFakeBones;
310 cheatItemsList[$] = ItemFishBone;
311 cheatItemsList[$] = ItemRock;
312 cheatItemsList[$] = ItemJar;
313 cheatItemsList[$] = ItemSkull;
314 cheatItemsList[$] = ItemGoldenKey;
315 cheatItemsList[$] = ItemGoldIdol;
316 cheatItemsList[$] = ItemCrystalSkull;
317 cheatItemsList[$] = ItemShellSingle;
318 cheatItemsList[$] = ItemChest;
319 cheatItemsList[$] = ItemCrate;
320 cheatItemsList[$] = ItemLockedChest;
321 cheatItemsList[$] = ItemDice;
322 cheatItemsList[$] = ItemBasketBall;
326 final UIPane createCheatItemsPane () {
327 if (!level.player) return none;
329 UIPane pane = SpawnObject(UIPane);
331 pane.sprStore = sprStore;
333 pane.width = 320*3-64;
334 pane.height = 240*3-64;
336 foreach (auto ipk; cheatItemsList) {
337 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
341 //optionsPaneOfs.x = 100;
342 //optionsPaneOfs.y = 50;
348 // ////////////////////////////////////////////////////////////////////////// //
349 transient array!(class!MapObject) cheatEnemiesList;
352 final void fillCheatEnemiesList () {
353 cheatEnemiesList.length = 0;
354 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
355 cheatEnemiesList[$] = EnemyBat;
356 cheatEnemiesList[$] = EnemySpiderHang;
357 cheatEnemiesList[$] = EnemySpider;
358 cheatEnemiesList[$] = EnemySnake;
359 cheatEnemiesList[$] = EnemyCaveman;
360 cheatEnemiesList[$] = EnemySkeleton;
361 cheatEnemiesList[$] = MonsterShopkeeper;
362 cheatEnemiesList[$] = EnemyZombie;
363 cheatEnemiesList[$] = EnemyVampire;
364 cheatEnemiesList[$] = EnemyFrog;
365 cheatEnemiesList[$] = EnemyGreenFrog;
366 cheatEnemiesList[$] = EnemyFireFrog;
367 cheatEnemiesList[$] = EnemyMantrap;
368 cheatEnemiesList[$] = EnemyScarab;
369 cheatEnemiesList[$] = EnemyFloater;
370 cheatEnemiesList[$] = EnemyBlob;
371 cheatEnemiesList[$] = EnemyMonkey;
372 cheatEnemiesList[$] = EnemyGoldMonkey;
373 cheatEnemiesList[$] = EnemyAlien;
374 cheatEnemiesList[$] = EnemyYeti;
375 cheatEnemiesList[$] = EnemyHawkman;
376 cheatEnemiesList[$] = EnemyUFO;
377 cheatEnemiesList[$] = EnemyYetiKing;
381 final UIPane createCheatEnemiesPane () {
382 if (!level.player) return none;
384 UIPane pane = SpawnObject(UIPane);
386 pane.sprStore = sprStore;
388 pane.width = 320*3-64;
389 pane.height = 240*3-64;
391 foreach (auto ipk; cheatEnemiesList) {
392 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
396 //optionsPaneOfs.x = 100;
397 //optionsPaneOfs.y = 50;
403 // ////////////////////////////////////////////////////////////////////////// //
404 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
407 final void fillCheatPickupList () {
408 cheatPickupList.length = 0;
409 cheatPickupList[$] = ItemPickupBombBag;
410 cheatPickupList[$] = ItemPickupBombBox;
411 cheatPickupList[$] = ItemPickupPaste;
412 cheatPickupList[$] = ItemPickupRopePile;
413 cheatPickupList[$] = ItemPickupShellBox;
414 cheatPickupList[$] = ItemPickupAnkh;
415 cheatPickupList[$] = ItemPickupCape;
416 cheatPickupList[$] = ItemPickupJetpack;
417 cheatPickupList[$] = ItemPickupUdjatEye;
418 cheatPickupList[$] = ItemPickupCrown;
419 cheatPickupList[$] = ItemPickupKapala;
420 cheatPickupList[$] = ItemPickupParachute;
421 cheatPickupList[$] = ItemPickupCompass;
422 cheatPickupList[$] = ItemPickupSpectacles;
423 cheatPickupList[$] = ItemPickupGloves;
424 cheatPickupList[$] = ItemPickupMitt;
425 cheatPickupList[$] = ItemPickupJordans;
426 cheatPickupList[$] = ItemPickupSpringShoes;
427 cheatPickupList[$] = ItemPickupSpikeShoes;
428 cheatPickupList[$] = ItemPickupTeleporter;
432 final UIPane createCheatPickupsPane () {
433 if (!level.player) return none;
435 UIPane pane = SpawnObject(UIPane);
437 pane.sprStore = sprStore;
439 pane.width = 320*3-64;
440 pane.height = 240*3-64;
442 foreach (auto ipk; cheatPickupList) {
443 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
447 //optionsPaneOfs.x = 100;
448 //optionsPaneOfs.y = 50;
454 // ////////////////////////////////////////////////////////////////////////// //
455 transient int instantGhost;
457 final UIPane createCheatFlagsPane () {
458 UIPane pane = SpawnObject(UIPane);
459 pane.id = 'CheatFlags';
460 pane.sprStore = sprStore;
462 pane.width = 320*3-64;
463 pane.height = 240*3-64;
467 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
468 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
469 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
470 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
471 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
472 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
473 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
474 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
475 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
476 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
477 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
478 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
479 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
480 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
481 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
482 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
483 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
484 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
486 optionsPaneOfs.x = 100;
487 optionsPaneOfs.y = 50;
493 final UIPane createOptionsPane () {
494 UIPane pane = SpawnObject(UIPane);
496 pane.sprStore = sprStore;
498 pane.width = 320*3-64;
499 pane.height = 240*3-64;
503 //!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.");
506 UILabel.Create(pane, "VISUAL OPTIONS");
507 UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
508 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
509 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).");
510 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
511 auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
512 startfs.onValueChanged = delegate void (int newval) {
513 Video.showMouseCursor();
518 auto fsmode = UIIntEnum.Create(pane, &config.fsmode, 1, 2, "FULLSCREEN MODE: ", "YOU CAN CHOOSE EITHER REAL FULLSCREEN MODE, OR SCALED. USUALLY, SCALED WORKS BETTER, BUT REAL LOOKS NICER (YET IT MAY NOT WORK ON YOUR GPU).");
519 fsmode.names[$] = "REAL";
520 fsmode.names[$] = "SCALED";
521 fsmode.onValueChanged = delegate void (int newval) {
523 Video.showMouseCursor();
530 UILabel.Create(pane, "");
531 UILabel.Create(pane, "HUD OPTIONS");
532 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
533 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.");
534 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
537 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
541 UILabel.Create(pane, "");
542 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
543 //!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.");
544 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
545 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
546 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.");
547 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.");
548 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
549 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.");
550 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
553 UILabel.Create(pane, "");
554 UILabel.Create(pane, "GAMEPLAY OPTIONS");
555 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.");
556 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
557 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
558 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!");
559 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.");
560 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.");
561 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
562 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.");
563 UICheckBox.Create(pane, &config.ghostRandom, "RANDOM GHOST DELAY", "THIS OPTION WILL RANDOMIZE THE DELAY UNTIL THE GHOST APPEARS AFTER THE TIME LIMIT BELOW IS REACHED INSTEAD OF USING THE DEFAULT 30 SECONDS. CHANGES EACH LEVEL AND VARIES WITH THE TIME LIMIT YOU SET.");
564 UICheckBox.Create(pane, &config.ghostAtFirstLevel, "GHOST AT FIRST LEVEL", "TURN THIS OPTION ON IF YOU WANT THE GHOST TO BE SPAWNED ON THE FIRST LEVEL.");
565 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.");
566 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?");
567 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.");
568 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.");
569 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.");
570 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
571 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
572 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
573 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
574 rstl.names[$] = "RANDOM";
575 rstl.names[$] = "NORMAL";
576 rstl.names[$] = "BIZARRE";
579 UILabel.Create(pane, "");
580 UILabel.Create(pane, "WHIP OPTIONS");
581 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
582 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
583 whiptype.names[$] = "NORMAL";
584 whiptype.names[$] = "LONG";
585 UICheckBox.Create(pane, &global.config.killEnemiesThruWalls, "PENETRATE WALLS", "WITH THIS OPTION ENABLED, YOU WILL BE ABLE TO WHIP ENEMIES THROUGH THE WALLS SOMETIMES. THIS IS HOW IT WORKED IN CLASSIC.");
588 UILabel.Create(pane, "");
589 UILabel.Create(pane, "PLAYER OPTIONS");
590 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
591 herotype.names[$] = "SPELUNKY GUY";
592 herotype.names[$] = "DAMSEL";
593 herotype.names[$] = "TUNNEL MAN";
596 UILabel.Create(pane, "");
597 UILabel.Create(pane, "CHEAT OPTIONS");
598 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.");
599 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
600 plrlit.names[$] = "NEVER";
601 plrlit.names[$] = "FORCED DARKNESS";
602 plrlit.names[$] = "ALWAYS";
603 UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
604 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'.");
605 rdark.names[$] = "NEVER";
606 rdark.names[$] = "DEFAULT";
607 rdark.names[$] = "ALWAYS";
608 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.");
610 rghost.getNameCB = delegate string (int val) {
611 if (val < 0) return "INSTANT";
612 if (val == 0) return "NEVER";
613 if (val < 120) return va("%d SEC", val);
614 if (val%60 == 0) return va("%d MIN", val/60);
615 if (val%60 == 30) return va("%d.5 MIN", val/60);
616 return va("%d MIN, %d SEC", val/60, val%60);
618 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.");
620 UILabel.Create(pane, "");
621 UILabel.Create(pane, "CHEAT START OPTIONS");
622 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.");
623 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
624 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
625 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
626 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
629 UILabel.Create(pane, "");
630 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
631 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
632 mm.names[$] = "SILENCE";
633 mm.names[$] = "RESTART";
634 mm.names[$] = "DON'T TOUCH";
636 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
637 //mm.names[$] = "SILENCE";
638 mm.names[$] = "RESTART";
639 mm.names[$] = "DON'T TOUCH";
642 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
644 swstereo.onValueChanged = delegate void (int newval) {
645 SoundSystem.SwapStereo = newval;
649 UILabel.Create(pane, "");
650 UILabel.Create(pane, "SOUND CONTROL CENTER");
651 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
652 rmusonoff.onValueChanged = delegate void (int newval) {
653 global.restartMusic();
656 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
658 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
659 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
661 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
662 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
665 saveOptionsDG = delegate void () {
666 writeln("saving options");
669 optionsPaneOfs.x = 42;
670 optionsPaneOfs.y = 0;
676 final void createBindingsControl (UIPane pane, int keyidx) {
679 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
680 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
681 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
682 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
683 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
684 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
685 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
686 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
687 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
688 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
689 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
692 int arridx = GameConfig.getKeyIndex(keyidx);
693 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
697 final UIPane createBindingsPane () {
698 UIPane pane = SpawnObject(UIPane);
699 pane.id = 'KeyBindings';
700 pane.sprStore = sprStore;
702 pane.width = 320*3-64;
703 pane.height = 240*3-64;
705 createBindingsControl(pane, GameConfig::Key.Left);
706 createBindingsControl(pane, GameConfig::Key.Right);
707 createBindingsControl(pane, GameConfig::Key.Up);
708 createBindingsControl(pane, GameConfig::Key.Down);
709 createBindingsControl(pane, GameConfig::Key.Jump);
710 createBindingsControl(pane, GameConfig::Key.Run);
711 createBindingsControl(pane, GameConfig::Key.Attack);
712 createBindingsControl(pane, GameConfig::Key.Switch);
713 createBindingsControl(pane, GameConfig::Key.Pay);
714 createBindingsControl(pane, GameConfig::Key.Bomb);
715 createBindingsControl(pane, GameConfig::Key.Rope);
717 saveOptionsDG = delegate void () {
718 writeln("saving keys");
719 saveKeyboardBindings();
721 optionsPaneOfs.x = 120;
722 optionsPaneOfs.y = 140;
728 // ////////////////////////////////////////////////////////////////////////// //
729 void clearGameMovement () {
730 debugMovement = SpawnObject(DebugSessionMovement);
731 debugMovement.playconfig = SpawnObject(GameConfig);
732 debugMovement.playconfig.copyGameplayConfigFrom(config);
733 debugMovement.resetReplay();
737 void saveGameMovement (string fname, optional bool packit) {
738 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
739 saveMovementLastTime = GetTickCount();
743 void loadGameMovement (string fname) {
744 delete debugMovement;
745 debugMovement = appLoadOptions(DebugSessionMovement, fname);
746 debugMovement.resetReplay();
749 origStats = level.stats;
750 origStats.global = none;
751 level.stats = SpawnObject(GameStats);
752 level.stats.global = global;
755 config = debugMovement.playconfig;
756 global.config = config;
757 origRoomSeed = global.globalRoomSeed;
758 origOtherSeed = global.globalOtherSeed;
759 writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
764 void stopReplaying () {
766 writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
767 global.globalRoomSeed = origRoomSeed;
768 global.globalOtherSeed = origOtherSeed;
770 delete debugMovement;
771 saveGameSession = false;
772 replayGameSession = false;
773 doGameSavingPlaying = Replay.None;
776 origStats.global = global;
777 level.stats = origStats;
783 global.config = origConfig;
789 // ////////////////////////////////////////////////////////////////////////// //
790 final bool saveGame (string gmname) {
791 return appSaveOptions(level, gmname);
795 final bool loadGame (string gmname) {
796 auto olddel = ImmediateDelete;
797 ImmediateDelete = false;
799 auto stats = level.stats;
802 auto lvl = appLoadOptions(GameLevel, gmname);
804 //lvl.global.config = config;
809 global = level.global;
810 global.config = config;
812 level.sprStore = sprStore;
813 level.bgtileStore = bgtileStore;
816 level.onBeforeFrame = &beforeNewFrame;
817 level.onAfterFrame = &afterNewFrame;
818 level.onInterFrame = &interFrame;
819 level.onLevelExitedCB = &levelExited;
820 level.onCameraTeleported = &cameraTeleportedCB;
822 //level.viewWidth = Video.screenWidth;
823 //level.viewHeight = Video.screenHeight;
824 level.viewWidth = 320*3;
825 level.viewHeight = 240*3;
828 level.centerViewAtPlayer();
829 teleportCameraAt(level.viewStart);
831 recalcCameraCoords(0);
836 level.stats.global = level.global;
838 ImmediateDelete = olddel;
839 CollectGarbage(true); // destroy delayed objects too
844 // ////////////////////////////////////////////////////////////////////////// //
845 float lastThinkerTime;
846 int replaySkipFrame = 0;
849 final void onTimePasses () {
850 float curTime = GetTickCount();
851 if (lastThinkerTime > 0) {
852 if (curTime < lastThinkerTime) {
853 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
854 lastThinkerTime = curTime;
857 if (replayFastForward && replaySkipFrame) {
859 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
862 level.processThinkers(curTime-lastThinkerTime);
864 lastThinkerTime = curTime;
868 final void resetFramesAndForceOne () {
869 float curTime = GetTickCount();
870 lastThinkerTime = curTime;
872 auto wasPaused = level.gamePaused;
873 level.gamePaused = false;
874 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
875 level.processThinkers(GameLevel::FrameTime);
876 level.gamePaused = wasPaused;
877 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
881 // ////////////////////////////////////////////////////////////////////////// //
882 private float currFrameDelta; // so level renderer can properly interpolate the player
883 private GameLevel::IVec2D camPrev, camCurr;
884 private GameLevel::IVec2D camShake;
885 private GameLevel::IVec2D viewCameraPos;
888 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
893 viewCameraPos.x = pos.x;
894 viewCameraPos.y = pos.y;
900 // call `recalcCameraCoords()` to get real camera coords after this
901 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
902 // check if camera is moved too far, and teleport it
904 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
905 abs(camCurr.y-pos.y)/global.scale >= 16*4))
907 teleportCameraAt(pos);
909 camPrev.x = camCurr.x;
910 camPrev.y = camCurr.y;
914 camShake.x = level.shakeDir.x*global.scale;
915 camShake.y = level.shakeDir.y*global.scale;
919 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
920 currFrameDelta = frameDelta;
921 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
922 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
924 viewCameraPos.x += camShake.x;
925 viewCameraPos.y += camShake.y;
929 GameLevel::SavedKeyState savedKeyState;
931 final void pauseGame () {
932 if (!level.gamePaused) {
933 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
934 level.gamePaused = true;
935 global.pauseAllSounds();
940 final void unpauseGame () {
941 if (level.gamePaused) {
942 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
943 level.gamePaused = false;
944 level.gameShowHelp = false;
945 level.gameHelpScreen = 0;
946 //lastThinkerTime = 0;
947 global.resumeAllSounds();
949 pauseRequested = false;
950 helpRequested = false;
955 final void beforeNewFrame (bool frameSkip) {
958 level.disablePlayerThink = true;
961 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
962 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
963 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
965 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
966 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
967 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
968 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
970 level.disablePlayerThink = false;
976 if (!level.gamePaused) {
977 // save seeds for afterframe processing
979 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
980 debugMovement.otherSeed = global.globalOtherSeed;
981 debugMovement.roomSeed = global.globalRoomSeed;
985 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
987 #ifdef BIGGER_REPLAY_DATA
988 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
989 debugMovement.keypresses.length += 1;
990 level.keysSaveState(debugMovement.keypresses[$-1]);
991 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
992 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
996 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
997 #ifdef BIGGER_REPLAY_DATA
998 if (debugMovement.keypos < debugMovement.keypresses.length) {
999 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1000 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1001 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1002 ++debugMovement.keypos;
1008 auto code = debugMovement.getKey(out kbidx, out down);
1009 if (code == DebugSessionMovement::END_OF_RECORD) {
1010 // do this in main loop, so we can view totals
1014 if (code == DebugSessionMovement::END_OF_FRAME) {
1017 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1018 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1026 final void afterNewFrame (bool frameSkip) {
1027 if (!replayFastForward) replaySkipFrame = 0;
1029 if (level.gamePaused) return;
1031 if (!level.gamePaused) {
1032 if (doGameSavingPlaying != Replay.None) {
1033 if (doGameSavingPlaying == Replay.Saving) {
1034 replayFastForward = false; // just in case
1035 #ifndef BIGGER_REPLAY_DATA
1036 debugMovement.addEndOfFrame();
1038 auto stt = GetTickCount();
1039 if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
1040 } else if (doGameSavingPlaying == Replay.Replaying) {
1041 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1042 replaySkipFrame = 1;
1048 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1049 //SoundSystem.UpdateSounds();
1051 //if (!freeRide) level.fixCamera();
1052 setNewCameraPos(level.viewStart);
1054 prevCameraX = currCameraX;
1055 prevCameraY = currCameraY;
1056 currCameraX = level.cameraX;
1057 currCameraY = level.cameraY;
1058 // disable camera interpolation if the screen is shaking
1059 if (level.shakeX|level.shakeY) {
1060 prevCameraX = currCameraX;
1061 prevCameraY = currCameraY;
1064 // disable camera interpolation if it moves too far away
1065 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1066 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1068 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1070 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1071 pauseRequested = false;
1073 if (helpRequested) {
1074 helpRequested = false;
1075 level.gameShowHelp = true;
1076 level.gameHelpScreen = 0;
1079 if (!showHelp) showHelp = true;
1086 final void interFrame (float frameDelta) {
1087 if (!config.interpolateMovement) return;
1088 recalcCameraCoords(frameDelta);
1092 final void cameraTeleportedCB () {
1093 teleportCameraAt(level.viewStart);
1094 recalcCameraCoords(0);
1098 // ////////////////////////////////////////////////////////////////////////// //
1100 final void setColorByIdx (bool isset, int col) {
1102 // missed collision: red
1103 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1104 } else if (col == -999) {
1105 // superfluous collision: blue
1106 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1107 } else if (col <= 0) {
1108 // no collision: yellow
1109 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1110 } else if (col > 0) {
1112 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1117 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1119 CollisionMask cm = CollisionMask.Create(frm, false);
1121 int scale = global.config.scale;
1122 int bx0, by0, bx1, by1;
1123 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1124 Video.color = 0x7f_00_00_ff;
1125 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1126 if (!cm.isEmptyMask) {
1127 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1128 foreach (int iy; 0..cm.height) {
1129 foreach (int ix; 0..cm.width) {
1130 int v = cm.mask[ix, iy];
1131 foreach (int dx; 0..32) {
1134 Video.color = 0x3f_00_ff_00;
1135 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1144 foreach (int iy; 0..frm.tex.height) {
1145 foreach (int ix; 0..(frm.tex.width+31)/31) {
1146 foreach (int dx; 0..32) {
1148 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1149 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1150 setColorByIdx(true, col);
1151 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1153 Video.color = 0xaf_00_ff_00;
1155 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1161 if (frm.bw > 0 && frm.bh > 0) {
1162 setColorByIdx(true, col);
1163 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1164 Video.color = 0xff_00_00;
1165 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1174 // ////////////////////////////////////////////////////////////////////////// //
1175 transient int drawStats;
1176 transient array!int statsTopItem;
1179 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1180 auto sa = string(a.objName).toUpperCase;
1181 auto sb = string(b.objName).toUpperCase;
1186 final int getStatsTopItem () {
1187 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1191 final void setStatsTopItem (int val) {
1192 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1193 statsTopItem[drawStats] = val;
1197 final void resetStatsTopItem () {
1202 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1203 sprStore.loadFont('sFontSmall');
1209 final int calcStatsVisItems () {
1212 statsDrawGetStartPosLoadFont(currX, currY);
1213 int endY = level.viewHeight-(currY*2);
1214 return max(1, endY/sprStore.getFontHeight(scale));
1218 int getStatsItemCount () {
1219 switch (drawStats) {
1220 case 2: return level.stats.totalKills.length;
1221 case 3: return level.stats.totalDeaths.length;
1222 case 4: return level.stats.totalCollected.length;
1228 final void statsMoveUp () {
1229 int count = getStatsItemCount();
1230 if (count < 0) return;
1231 int visItems = calcStatsVisItems();
1232 if (count <= visItems) { resetStatsTopItem(); return; }
1233 int top = getStatsTopItem();
1235 setStatsTopItem(top-1);
1239 final void statsMoveDown () {
1240 int count = getStatsItemCount();
1241 if (count < 0) return;
1242 int visItems = calcStatsVisItems();
1243 if (count <= visItems) { resetStatsTopItem(); return; }
1244 int top = getStatsTopItem();
1245 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1246 top = clamp(top+1, 0, count-visItems);
1247 setStatsTopItem(top);
1251 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1252 arr.sort(&totalsNameCmpCB);
1256 statsDrawGetStartPosLoadFont(currX, currY);
1258 int endY = level.viewHeight-(currY*2);
1259 int visItems = calcStatsVisItems();
1261 if (arr.length <= visItems) resetStatsTopItem();
1263 int topItem = getStatsTopItem();
1267 Video.color = 0x3f_ff_ff_00;
1268 auto spr = sprStore['sPageUp'];
1269 spr.frames[0].tex.blitAt(currX-28, currY, scale);
1272 // "downscroll" mark
1273 if (topItem+visItems < arr.length) {
1274 Video.color = 0x3f_ff_ff_00;
1275 auto spr = sprStore['sPageDown'];
1276 spr.frames[0].tex.blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1279 Video.color = 0xff_ff_00;
1280 int hiColor = 0x00_ff_00;
1281 int hiColor1 = 0xf_ff_ff;
1284 while (it < arr.length && visItems-- > 0) {
1285 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);
1286 currY += sprStore.getFontHeight(scale);
1292 void drawStatsScreen () {
1293 int deathCount, killCount, collectCount;
1295 sprStore.loadFont('sFontSmall');
1297 Video.color = 0xff_ff_ff;
1298 level.drawTextAtS3Centered(240-2-8, "ESC-RETURN F10-QUIT CTRL+DEL-SUICIDE");
1299 level.drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
1301 Video.color = 0xff_ff_00;
1302 int hiColor = 0x00_ff_00;
1304 switch (drawStats) {
1305 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1306 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1307 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1310 if (drawStats > 1) {
1312 foreach (ref auto i; statsTopItem) i = 0;
1317 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1318 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1319 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1325 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1326 currY += sprStore.getFontHeight(scale);
1328 int gw = level.stats.gamesWon;
1329 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1330 currY += sprStore.getFontHeight(scale);
1332 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1333 currY += sprStore.getFontHeight(scale);
1335 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1336 currY += sprStore.getFontHeight(scale);
1338 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1339 currY += sprStore.getFontHeight(scale);
1341 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1342 currY += sprStore.getFontHeight(scale);
1344 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1345 currY += sprStore.getFontHeight(scale);
1347 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1348 currY += sprStore.getFontHeight(scale);
1350 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1351 currY += sprStore.getFontHeight(scale);
1353 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1354 currY += sprStore.getFontHeight(scale);
1356 int gs = level.stats.totalGhostSummoned;
1357 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1358 currY += sprStore.getFontHeight(scale);
1360 currY += sprStore.getFontHeight(scale);
1361 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1362 currY += sprStore.getFontHeight(scale);
1367 if (Video.frameTime == 0) {
1369 Video.requestRefresh();
1374 if (level.framesProcessedFromLastClear < 1) return;
1375 calcMouseMapCoords();
1377 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1378 Video.clearScreen();
1379 Video.stencil = false;
1380 Video.color = 0xff_ff_ff;
1381 Video.textureFiltering = false;
1382 // don't touch framebuffer alpha
1383 Video.colorMask = Video::CMask.Colors;
1385 Video::ScissorRect scsave;
1386 bool doRestoreGL = false;
1389 if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1391 Video.getScissor(scsave);
1392 Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1393 Video.glPushMatrix();
1394 Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1398 if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1400 float scx = float(Video.screenWidth)/float(level.viewWidth);
1401 float scy = float(Video.screenHeight)/float(level.viewHeight);
1402 float scale = fmin(scx, scy);
1403 int calcedW = trunc(level.viewWidth*scale);
1404 int calcedH = trunc(level.viewHeight*scale);
1405 Video.getScissor(scsave);
1406 int ofsx = (Video.screenWidth-calcedW)/2;
1407 int ofsy = (Video.screenHeight-calcedH)/2;
1408 Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1409 Video.glPushMatrix();
1410 Video.glTranslate(ofsx, ofsy);
1411 Video.glScale(scale, scale);
1414 //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1415 //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1419 level.viewOffsetX = 0;
1420 level.viewOffsetY = 0;
1421 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1424 float scx = float(Video.screenWidth)/float(level.viewWidth);
1425 float scy = float(Video.screenHeight)/float(level.viewHeight);
1426 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1431 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1433 if (level.gamePaused && showHelp != 2) {
1434 if (mouseLevelX != int.min) {
1435 int scale = level.global.scale;
1436 if (renderMouseRect) {
1437 Video.color = 0xcf_ff_ff_00;
1438 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1440 if (renderMouseTile) {
1441 Video.color = 0xaf_ff_00_00;
1442 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1447 switch (doGameSavingPlaying) {
1449 Video.color = 0x7f_00_ff_00;
1450 sprStore.loadFont('sFont');
1451 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1453 case Replay.Replaying:
1454 if (level.player && !level.player.dead) {
1455 Video.color = 0x7f_ff_00_00;
1456 sprStore.loadFont('sFont');
1457 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1458 int th = sprStore.getFontHeight(2);
1459 if (replayFastForward) {
1460 sprStore.loadFont('sFontSmall');
1461 string sstr = va("x%d", replayFastForwardSpeed+1);
1462 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1467 if (saveGameSession) {
1468 Video.color = 0x7f_ff_7f_00;
1469 sprStore.loadFont('sFont');
1470 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1476 if (level.player && level.player.dead && !showHelp) {
1478 Video.color = 0x8f_00_00_00;
1479 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1484 if (true /*level.inWinCutscene == 0*/) {
1485 Video.color = 0xff_ff_ff;
1486 sprStore.loadFont('sFontSmall');
1487 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1489 "PRESS $PAY TO RESTART GAME\n"~
1491 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1493 "TOTAL PLAYING TIME: |%s|"~
1495 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1496 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1497 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1499 GameLevel.time2str(level.stats.playingTime)
1501 kmsg = global.expandString(kmsg);
1502 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1509 Video.color = 0xff_7f_00;
1510 sprStore.loadFont('sFontSmall');
1511 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1512 auto spf = smask.frames[maskFrame];
1513 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1515 spf.bx, spf.by, spf.bw, spf.bh,
1516 (spf.maskEmpty ? "TAN" : "ONA"),
1517 (spf.precise ? "TAN" : "ONA")),
1520 //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1521 //writeln("pos=(", maskSX, ",", maskSY, ")");
1522 int scale = global.config.scale;
1523 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1524 int mapX = xofs/scale+maskSX;
1525 int mapY = yofs/scale+maskSY;
1528 writeln("==== tiles ====");
1530 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1531 if (t.spectral || !t.isInstanceAlive) return false;
1532 Video.color = 0x7f_ff_00_00;
1533 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);
1534 auto tsf = t.getSpriteFrame();
1536 auto spf = smask.frames[maskFrame];
1537 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1538 int mapX = xofs/global.config.scale+maskSX;
1539 int mapY = yofs/global.config.scale+maskSY;
1542 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1543 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1544 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1548 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1549 Video.color = 0x7f_ff_00_00;
1550 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);
1554 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1556 Video.color = 0xaf_ff_ff_ff;
1557 spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1558 Video.color = 0xff_ff_00;
1559 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1563 int fx0, fy0, fx1, fy1;
1564 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1565 Video.color = 0x7f_00_00_ff;
1566 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1572 Video.color = 0x8f_00_00_00;
1573 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1575 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1580 Video.color = 0xff_ff_00;
1581 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1582 if (showHelp == 1) {
1583 int msx, msy, ww, wh;
1584 Video.getMousePos(out msx, out msy);
1585 Video.getRealWindowSize(out ww, out wh);
1586 if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1587 sprStore.loadFont('sFontSmall');
1588 Video.color = 0xff_ff_00;
1589 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1590 "F1: show this help\n"~
1592 "K : redefine keys\n"~
1593 "I : toggle interpolaion\n"~
1594 "N : create some blood\n"~
1595 "R : generate a new level\n"~
1596 "F : toggle \"Frozen Area\"\n"~
1597 "X : resurrect player\n"~
1598 "Q : teleport to exit\n"~
1599 "D : teleport to damel\n"~
1601 "C : cheat flags menu\n"~
1602 "P : cheat pickup menu\n"~
1603 "E : cheat enemy menu\n"~
1604 "Enter: cheat items menu\n"~
1606 "TAB: toggle 'freeroam' mode\n"~
1611 if (level) level.renderPauseOverlay();
1615 //SoundSystem.UpdateSounds();
1617 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1620 Video.setScissor(scsave);
1621 Video.glPopMatrix();
1626 Video.color = 0xaf_ff_ff_ff;
1627 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1632 // ////////////////////////////////////////////////////////////////////////// //
1633 transient bool gameJustOver;
1634 transient bool waitingForPayRestart;
1637 final void calcMouseMapCoords () {
1638 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1639 mouseLevelX = int.min;
1640 mouseLevelY = int.min;
1643 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1644 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1645 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1649 final void onEvent (ref event_t evt) {
1650 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1652 if (evt.type == ev_winfocus) {
1653 if (level && !evt.focused) {
1658 //writeln("FOCUS!");
1659 Video.getMousePos(out mouseX, out mouseY);
1664 if (evt.type == ev_mouse) {
1667 calcMouseMapCoords();
1670 if (evt.type == ev_keydown && evt.keycode == K_F12) {
1671 if (level) toggleFullscreen();
1675 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1676 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1677 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1680 if (evt.type == ev_keydown) {
1681 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1682 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1683 renderMouseTile = evt.bCtrl;
1684 renderMouseRect = evt.bAlt;
1687 if (evt.type == ev_keyup) {
1688 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1689 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1690 renderMouseTile = evt.bCtrl;
1691 renderMouseRect = evt.bAlt;
1694 if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1696 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1697 int newScale = evt.keycode-48;
1698 if (global.config.scale != newScale) {
1699 global.config.scale = newScale;
1702 cameraTeleportedCB();
1709 if (evt.type == ev_mouse) {
1710 maskSX = evt.x/global.config.scale;
1711 maskSY = evt.y/global.config.scale;
1714 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1715 maskFrame = max(0, maskFrame-1);
1718 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1719 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1728 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1730 if (saveOptionsDG) saveOptionsDG();
1731 saveOptionsDG = none;
1733 //SoundSystem.UpdateSounds(); // just in case
1734 if (global.hasSpectacles) level.pickedSpectacles();
1737 optionsPane.onEvent(evt);
1741 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1742 if (evt.type == ev_keydown) {
1743 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1744 switch (evt.keycode) {
1745 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1746 case K_F2: if (showHelp != 2) unpauseGame(); return;
1747 case K_F10: Video.requestQuit(); return;
1748 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1750 case K_UPARROW: case K_PAD8:
1751 if (drawStats) statsMoveUp();
1754 case K_DOWNARROW: case K_PAD2:
1755 if (drawStats) statsMoveDown();
1758 case K_LEFTARROW: case K_PAD4:
1759 if (level && showHelp == 2 && level.gameShowHelp) {
1760 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1764 case K_RIGHTARROW: case K_PAD6:
1765 if (level && showHelp == 2 && level.gameShowHelp) {
1766 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1780 resetFramesAndForceOne();
1786 if (/*evt.bCtrl &&*/ showHelp != 2) {
1796 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1797 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1798 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1799 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1800 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1801 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1802 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1803 //case K_j: global.hasJordans = !global.hasJordans; return;
1805 if (/*evt.bCtrl &&*/ showHelp != 2) {
1806 level.resurrectPlayer();
1811 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1812 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1813 if (evt.bAlt && level.player && level.player.dead) {
1814 saveGameSession = false;
1815 replayGameSession = true;
1819 if (/*evt.bCtrl &&*/ showHelp != 2) {
1820 if (evt.bShift) global.idol = false;
1821 level.generateLevel();
1822 level.centerViewAtPlayer();
1823 teleportCameraAt(level.viewStart);
1824 resetFramesAndForceOne();
1828 global.toggleMusic();
1831 if (/*evt.bCtrl &&*/ showHelp != 2) {
1832 if (level.allExits.length) {
1833 level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1839 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1840 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1842 level.teleportPlayerTo(damsel.ix, damsel.iy);
1848 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1852 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1855 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1858 level.teleportPlayerTo(obj.ix, obj.iy-4);
1864 if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1865 if (level && mouseLevelX != int.min) {
1866 int scale = level.global.scale;
1867 int mapX = mouseLevelX;
1868 int mapY = mouseLevelY;
1869 level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1875 if (evt.bCtrl && showHelp != 2) {
1876 if (level && mouseLevelX != int.min) {
1877 int scale = level.global.scale;
1878 int mapX = mouseLevelX;
1879 int mapY = mouseLevelY;
1880 level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1886 if (evt.bCtrl && showHelp != 2) {
1887 if (level && mouseLevelX != int.min) {
1888 int scale = level.global.scale;
1889 int mapX = mouseLevelX;
1890 int mapY = mouseLevelY;
1891 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1895 if (evt.bAlt && showHelp != 2) {
1896 if (level && mouseLevelX != int.min) {
1897 int scale = level.global.scale;
1898 int mapX = mouseLevelX;
1899 int mapY = mouseLevelY;
1900 level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1906 if (level && mouseLevelX != int.min) {
1907 int scale = level.global.scale;
1908 int mapX = mouseLevelX;
1909 int mapY = mouseLevelY;
1912 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1913 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1914 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1918 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1919 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1925 if (/*evt.bAlt &&*/ showHelp != 2) {
1926 auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1927 //if (obj) obj.monkey = monkey;
1929 //playSound('sndThump');
1935 case K_DELETE: // suicide
1936 if (doGameSavingPlaying == Replay.None) {
1937 if (level.player && !level.player.dead && evt.bCtrl) {
1938 global.hasAnkh = false;
1939 level.global.plife = 1;
1940 level.player.invincible = 0;
1941 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1942 if (xplo) xplo.suicide = true;
1949 if (level.player && !level.player.dead && evt.bAlt) {
1950 if (doGameSavingPlaying != Replay.None) {
1951 if (doGameSavingPlaying == Replay.Replaying) {
1953 } else if (doGameSavingPlaying == Replay.Saving) {
1954 saveGameMovement(dbgSessionMovementFileName, packit:true);
1956 doGameSavingPlaying = Replay.None;
1958 saveGameSession = false;
1959 replayGameSession = false;
1966 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1967 level.stats.setMoneyCheat();
1968 level.stats.addMoney(10000);
1974 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1975 if (level.player && level.player.dead) {
1976 //Video.requestQuit();
1978 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1980 #ifdef QUIT_DOUBLE_ESC
1981 if (++escCount == 2) Video.requestQuit();
1984 pauseRequested = true;
1990 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
1991 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1992 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1995 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1998 if (!level.player || !level.player.dead) {
1999 gameJustOver = false;
2000 } else if (level.player && level.player.dead) {
2001 if (!gameJustOver) {
2003 gameJustOver = true;
2004 waitingForPayRestart = true;
2005 level.clearKeysPressRelease();
2006 if (doGameSavingPlaying == Replay.None) {
2007 stopReplaying(); // just in case
2011 replayFastForward = false;
2012 if (doGameSavingPlaying == Replay.Saving) {
2013 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2014 doGameSavingPlaying = Replay.None;
2015 //clearGameMovement();
2016 saveGameSession = false;
2017 replayGameSession = false;
2020 if (evt.type == ev_keydown || evt.type == ev_keyup) {
2021 bool down = (evt.type == ev_keydown);
2022 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2023 if (down && evt.keycode == K_f) {
2025 if (replayFastForwardSpeed != 4) {
2026 replayFastForwardSpeed = 4;
2027 replayFastForward = true;
2029 replayFastForward = !replayFastForward;
2032 replayFastForwardSpeed = 2;
2033 replayFastForward = !replayFastForward;
2037 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2038 foreach (int kbidx, int kval; global.config.keybinds) {
2039 if (kval && kval == evt.keycode) {
2040 #ifndef BIGGER_REPLAY_DATA
2041 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2043 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2047 if (level.player && level.player.dead) {
2048 if (down && evt.keycode == K_r && evt.bAlt) {
2049 saveGameSession = false;
2050 replayGameSession = true;
2053 if (down && evt.keycode == K_s && evt.bAlt) {
2054 bool wasSaveReq = saveGameSession;
2055 stopReplaying(); // just in case
2056 saveGameSession = !wasSaveReq;
2057 replayGameSession = false;
2060 if (replayGameSession) {
2061 stopReplaying(); // just in case
2062 saveGameSession = false;
2063 replayGameSession = false;
2064 loadGameMovement(dbgSessionMovementFileName);
2065 loadGame(dbgSessionStateFileName);
2066 doGameSavingPlaying = Replay.Replaying;
2068 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2069 if (waitingForPayRestart) {
2070 level.isKeyReleased(GameConfig::Key.Pay);
2071 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2073 level.isKeyPressed(GameConfig::Key.Pay);
2074 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2075 auto doSave = saveGameSession;
2076 stopReplaying(); // just in case
2077 level.clearKeysPressRelease();
2078 level.restartGame();
2079 level.generateNormalLevel();
2081 saveGameSession = false;
2082 replayGameSession = false;
2083 writeln("DBG: saving game session...");
2084 clearGameMovement();
2085 doGameSavingPlaying = Replay.Saving;
2086 saveGame(dbgSessionStateFileName);
2087 //saveGameMovement(dbgSessionMovementFileName);
2098 void levelExited () {
2104 void initializeVideo () {
2105 Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2106 if (Video.realStencilBits < 8) {
2107 Video.closeScreen();
2108 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2110 if (!Video.framebufferHasAlpha) {
2111 Video.closeScreen();
2112 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!");
2114 if (fullscreen) Video.hideMouseCursor();
2118 void toggleFullscreen () {
2119 Video.showMouseCursor();
2120 Video.closeScreen();
2121 fullscreen = !fullscreen;
2126 final void runGameLoop () {
2127 Video.frameTime = 0; // unlimited FPS
2128 lastThinkerTime = 0;
2130 sprStore = SpawnObject(SpriteStore);
2131 sprStore.bDumpLoaded = false;
2133 bgtileStore = SpawnObject(BackTileStore);
2134 bgtileStore.bDumpLoaded = false;
2136 level = SpawnObject(GameLevel);
2137 level.setup(global, sprStore, bgtileStore);
2139 level.BuildYear = BuildYear;
2140 level.BuildMonth = BuildMonth;
2141 level.BuildDay = BuildDay;
2142 level.BuildHour = BuildHour;
2143 level.BuildMin = BuildMin;
2145 level.global = global;
2146 level.sprStore = sprStore;
2147 level.bgtileStore = bgtileStore;
2150 //level.stats.introViewed = 0;
2152 if (level.stats.introViewed == 0) {
2153 startMode = StartMode.Intro;
2154 writeln("FORCED INTRO");
2156 //writeln("INTRO VIWED: ", level.stats.introViewed);
2157 if (level.global.config.skipIntro) startMode = StartMode.Title;
2160 level.onBeforeFrame = &beforeNewFrame;
2161 level.onAfterFrame = &afterNewFrame;
2162 level.onInterFrame = &interFrame;
2163 level.onLevelExitedCB = &levelExited;
2164 level.onCameraTeleported = &cameraTeleportedCB;
2167 maskSX = -0x0ff_fff;
2169 smask = sprStore['sExplosionMask'];
2173 sprStore.loadFont('sFontSmall');
2175 level.viewWidth = 320*3;
2176 level.viewHeight = 240*3;
2178 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2179 //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2180 fullscreen = global.config.startFullscreen;
2183 //SoundSystem.SwapStereo = config.swapStereo;
2184 SoundSystem.NumChannels = 32;
2185 SoundSystem.MaxHearingDistance = 12000;
2186 //SoundSystem.DopplerFactor = 1.0f;
2187 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2188 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2189 SoundSystem.ReferenceDistance = 16.0f*4;
2190 SoundSystem.MaxDistance = 16.0f*(5*10);
2192 SoundSystem.Initialize();
2193 if (!SoundSystem.IsInitialized) {
2194 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2195 global.soundDisabled = true;
2196 global.musicDisabled = true;
2198 global.fixVolumes();
2200 level.restartGame(); // this will NOT generate a new level
2205 texTigerEye = GLTexture.Load("teye0.png");
2207 if (global.cheatEndGameSequence) {
2208 level.winTime = 12*60+42;
2209 level.stats.money = 6666;
2210 switch (global.cheatEndGameSequence) {
2211 case 1: default: level.startWinCutscene(); break;
2212 case 2: level.startWinCutsceneVolcano(); break;
2213 case 3: level.startWinCutsceneWinFall(); break;
2216 switch (startMode) {
2217 case StartMode.Title: level.restartTitle(); break;
2218 case StartMode.Intro: level.restartIntro(); break;
2219 case StartMode.Stars: level.restartStarsRoom(); break;
2220 case StartMode.Sun: level.restartSunRoom(); break;
2221 case StartMode.Moon: level.restartMoonRoom(); break;
2223 level.generateNormalLevel();
2224 if (startMode == StartMode.Dead) {
2225 level.player.dead = true;
2226 level.player.visible = false;
2232 //global.rope = 666;
2233 //global.bombs = 666;
2235 //global.globalRoomSeed = 871520037;
2236 //global.globalOtherSeed = 1047036290;
2238 //level.createTitleRoom();
2239 //level.createTrans4Room();
2240 //level.createOlmecRoom();
2241 //level.generateLevel();
2243 //level.centerViewAtPlayer();
2244 teleportCameraAt(level.viewStart);
2245 //writeln(Video.swapInterval);
2247 Video.runEventLoop();
2248 Video.showMouseCursor();
2249 Video.closeScreen();
2250 SoundSystem.Shutdown();
2252 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2260 // ////////////////////////////////////////////////////////////////////////// //
2261 // duplicates are not allowed!
2262 final void checkGameObjNames () {
2263 array!(class!Object) known;
2265 int classCount = 0, namedCount = 0;
2266 foreach AllClasses(Object, out cc) {
2267 auto gn = GetClassGameObjName(cc);
2269 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2270 auto nid = NameToInt(gn);
2271 if (nid < known.length && known[nid]) FatalError("duplicate game object name '%n' (defined for class is '%n', redefined in class '%n')", gn, GetClassName(known[nid]), GetClassName(cc));
2277 writeln(classCount, " classes, ", namedCount, " game object classes.");
2281 // ////////////////////////////////////////////////////////////////////////// //
2282 #include "timelimit.vc"
2283 //const int TimeLimitDate = 2018232;
2286 void performTimeCheck () {
2287 #ifdef DISABLE_TIME_CHECK
2289 if (TigerEye) return;
2292 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2295 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2297 int tldate = tm.year*1000+tm.yday;
2299 if (tldate > TimeLimitDate) {
2300 level.maxPlayingTime = 24;
2302 //writeln("*** days left: ", TimeLimitDate-tldate);
2308 void setupCheats () {
2311 startMode = StartMode.Alive;
2312 global.currLevel = 13;
2313 global.config.scale = 1;
2314 global.cityOfGold = true;
2317 startMode = StartMode.Alive;
2318 global.currLevel = 5;
2319 global.genBlackMarket = true;
2322 startMode = StartMode.Alive;
2323 global.currLevel = 2;
2324 global.scumGenShop = true;
2325 global.scumGenShopType = GameGlobal::ShopType.Weapon;
2326 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2327 //global.config.scale = 1;
2330 //startMode = StartMode.Intro;
2333 global.currLevel = 2;
2334 startMode = StartMode.Alive;
2337 global.currLevel = 5;
2338 startMode = StartMode.Alive;
2339 global.scumGenLake = true;
2340 global.config.scale = 1;
2343 startMode = StartMode.Alive;
2344 global.cheatCanSkipOlmec = true;
2345 global.currLevel = 16;
2346 //global.currLevel = 5;
2347 //global.currLevel = 13;
2348 //global.config.scale = 1;
2350 //startMode = StartMode.Dead;
2351 //startMode = StartMode.Title;
2352 //startMode = StartMode.Stars;
2353 //startMode = StartMode.Sun;
2354 startMode = StartMode.Moon;
2356 //global.scumGenSacrificePit = true;
2357 //global.scumAlwaysSacrificeAltar = true;
2359 // first lush jungle level
2360 //global.levelType = 1;
2362 global.scumGenCemetary = true;
2364 //global.idol = false;
2365 //global.currLevel = 5;
2367 //global.isTunnelMan = true;
2370 //global.currLevel = 5;
2371 //global.scumGenLake = true;
2373 //global.currLevel = 5;
2374 //global.currLevel = 9;
2375 //global.currLevel = 13;
2376 //global.currLevel = 14;
2377 //global.cheatEndGameSequence = 1;
2380 //global.currLevel = 6;
2381 global.scumGenAlienCraft = true;
2382 global.currLevel = 9;
2383 //global.scumGenYetiLair = true;
2384 //global.genBlackMarket = true;
2385 //startDead = false;
2386 startMode = StartMode.Alive;
2389 global.cheatCanSkipOlmec = true;
2390 global.currLevel = 15;
2391 startMode = StartMode.Alive;
2394 global.scumGenShop = true;
2395 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2396 global.scumGenShopType = GameGlobal::ShopType.Craps;
2397 //global.scumGenShopType = 6; // craps
2398 //global.scumGenShopType = 7; // kissing
2400 //global.scumAlwaysSacrificeAltar = true;
2404 void setupSeeds () {
2408 // ////////////////////////////////////////////////////////////////////////// //
2410 checkGameObjNames();
2412 appSetName("k8spelunky");
2413 config = SpawnObject(GameConfig);
2414 global = SpawnObject(GameGlobal);
2415 global.config = config;
2416 config.heroType = GameConfig::Hero.Spelunker;
2418 global.randomizeSeedAll();
2420 fillCheatPickupList();
2421 fillCheatItemsList();
2422 fillCheatEnemiesList();
2425 loadKeyboardBindings();