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;
105 // ////////////////////////////////////////////////////////////////////////// //
108 transient string dbgSessionStateFileName = "debug_game_session_state";
109 transient string dbgSessionMovementFileName = "debug_game_session_movement";
111 GLTexture texTigerEye;
115 SpriteStore sprStore;
116 BackTileStore bgtileStore;
119 int mouseX = int.min, mouseY = int.min;
120 int mouseLevelX = int.min, mouseLevelY = int.min;
121 bool renderMouseTile;
122 bool renderMouseRect;
134 StartMode startMode = StartMode.Intro;
138 bool replayFastForward = false;
139 int replayFastForwardSpeed = 2;
140 bool saveGameSession = false;
141 bool replayGameSession = false;
147 Replay doGameSavingPlaying = Replay.None;
148 float saveMovementLastTime = 0;
149 DebugSessionMovement debugMovement;
150 GameStats origStats; // for replaying
151 GameConfig origConfig; // for replaying
152 int origRoomSeed, origOtherSeed;
160 transient int maskSX, maskSY;
161 transient SpriteImage smask;
162 transient int maskFrame;
166 // ////////////////////////////////////////////////////////////////////////// //
167 final void saveKeyboardBindings () {
168 auto tok = SpawnObject(TempOptionsKeys);
169 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
170 appSaveOptions(tok, "keybindings");
175 final void loadKeyboardBindings () {
176 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
178 if (tok.kbversion != TempOptionsKeys.default.kbversion) {
179 global.config.resetKeybindings();
181 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
188 // ////////////////////////////////////////////////////////////////////////// //
189 void saveGameOptions () {
190 appSaveOptions(global.config, "config");
194 void loadGameOptions () {
195 auto cfg = appLoadOptions(GameConfig, "config");
197 auto oldHero = config.heroType;
198 auto tok = SpawnObject(TempOptionsKeys);
199 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
200 delete global.config;
203 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
205 writeln("config loaded");
206 global.restartMusic();
208 //config.heroType = GameConfig::Hero.Spelunker;
209 config.heroType = oldHero;
212 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
216 // ////////////////////////////////////////////////////////////////////////// //
217 void saveGameStats () {
218 if (level.stats) appSaveOptions(level.stats, "stats");
222 void loadGameStats () {
223 auto stats = appLoadOptions(GameStats, "stats");
228 if (!level.stats) level.stats = SpawnObject(GameStats);
229 level.stats.global = global;
233 // ////////////////////////////////////////////////////////////////////////// //
234 struct UIPaneSaveInfo {
236 UIPane::SaveInfo nfo;
239 transient UIPane optionsPane; // either options, or binding editor
241 transient GameLevel::IVec2D optionsPaneOfs;
242 transient void delegate () saveOptionsDG;
244 transient array!UIPaneSaveInfo optionsPaneState;
247 final void saveCurrentPane () {
248 if (!optionsPane || !optionsPane.id) return;
251 if (optionsPane.id == 'CheatFlags') {
252 if (instantGhost && level.ghostTimeLeft > 0) {
253 level.ghostTimeLeft = 1;
257 foreach (ref auto psv; optionsPaneState) {
258 if (psv.id == optionsPane.id) {
259 optionsPane.saveState(psv.nfo);
264 optionsPaneState.length += 1;
265 optionsPaneState[$-1].id = optionsPane.id;
266 optionsPane.saveState(optionsPaneState[$-1].nfo);
270 final void restoreCurrentPane () {
271 if (optionsPane) optionsPane.setupHotkeys(); // why not?
272 if (!optionsPane || !optionsPane.id) return;
273 foreach (ref auto psv; optionsPaneState) {
274 if (psv.id == optionsPane.id) {
275 optionsPane.restoreState(psv.nfo);
282 // ////////////////////////////////////////////////////////////////////////// //
283 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
284 if (!it.tagClass) return;
285 if (class!MapObject(it.tagClass)) {
286 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
287 it.owner.closeMe = true;
292 // ////////////////////////////////////////////////////////////////////////// //
293 transient array!(class!MapObject) cheatItemsList;
296 final void fillCheatItemsList () {
297 cheatItemsList.length = 0;
298 cheatItemsList[$] = ItemProjectileArrow;
299 cheatItemsList[$] = ItemWeaponShotgun;
300 cheatItemsList[$] = ItemWeaponAshShotgun;
301 cheatItemsList[$] = ItemWeaponPistol;
302 cheatItemsList[$] = ItemWeaponMattock;
303 cheatItemsList[$] = ItemWeaponMachete;
304 cheatItemsList[$] = ItemWeaponWebCannon;
305 cheatItemsList[$] = ItemWeaponSceptre;
306 cheatItemsList[$] = ItemWeaponBow;
307 cheatItemsList[$] = ItemBones;
308 cheatItemsList[$] = ItemFakeBones;
309 cheatItemsList[$] = ItemFishBone;
310 cheatItemsList[$] = ItemRock;
311 cheatItemsList[$] = ItemJar;
312 cheatItemsList[$] = ItemSkull;
313 cheatItemsList[$] = ItemGoldenKey;
314 cheatItemsList[$] = ItemGoldIdol;
315 cheatItemsList[$] = ItemCrystalSkull;
316 cheatItemsList[$] = ItemShellSingle;
317 cheatItemsList[$] = ItemChest;
318 cheatItemsList[$] = ItemCrate;
319 cheatItemsList[$] = ItemLockedChest;
320 cheatItemsList[$] = ItemDice;
321 cheatItemsList[$] = ItemBasketBall;
325 final UIPane createCheatItemsPane () {
326 if (!level.player) return none;
328 UIPane pane = SpawnObject(UIPane);
330 pane.sprStore = sprStore;
332 pane.width = 320*3-64;
333 pane.height = 240*3-64;
335 foreach (auto ipk; cheatItemsList) {
336 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
340 //optionsPaneOfs.x = 100;
341 //optionsPaneOfs.y = 50;
347 // ////////////////////////////////////////////////////////////////////////// //
348 transient array!(class!MapObject) cheatEnemiesList;
351 final void fillCheatEnemiesList () {
352 cheatEnemiesList.length = 0;
353 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
354 cheatEnemiesList[$] = EnemyBat;
355 cheatEnemiesList[$] = EnemySpiderHang;
356 cheatEnemiesList[$] = EnemySpider;
357 cheatEnemiesList[$] = EnemySnake;
358 cheatEnemiesList[$] = EnemyCaveman;
359 cheatEnemiesList[$] = EnemySkeleton;
360 cheatEnemiesList[$] = MonsterShopkeeper;
361 cheatEnemiesList[$] = EnemyZombie;
362 cheatEnemiesList[$] = EnemyVampire;
363 cheatEnemiesList[$] = EnemyFrog;
364 cheatEnemiesList[$] = EnemyGreenFrog;
365 cheatEnemiesList[$] = EnemyFireFrog;
366 cheatEnemiesList[$] = EnemyMantrap;
367 cheatEnemiesList[$] = EnemyScarab;
368 cheatEnemiesList[$] = EnemyFloater;
369 cheatEnemiesList[$] = EnemyBlob;
370 cheatEnemiesList[$] = EnemyMonkey;
371 cheatEnemiesList[$] = EnemyGoldMonkey;
372 cheatEnemiesList[$] = EnemyAlien;
373 cheatEnemiesList[$] = EnemyYeti;
374 cheatEnemiesList[$] = EnemyHawkman;
375 cheatEnemiesList[$] = EnemyUFO;
376 cheatEnemiesList[$] = EnemyYetiKing;
380 final UIPane createCheatEnemiesPane () {
381 if (!level.player) return none;
383 UIPane pane = SpawnObject(UIPane);
385 pane.sprStore = sprStore;
387 pane.width = 320*3-64;
388 pane.height = 240*3-64;
390 foreach (auto ipk; cheatEnemiesList) {
391 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
395 //optionsPaneOfs.x = 100;
396 //optionsPaneOfs.y = 50;
402 // ////////////////////////////////////////////////////////////////////////// //
403 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
406 final void fillCheatPickupList () {
407 cheatPickupList.length = 0;
408 cheatPickupList[$] = ItemPickupBombBag;
409 cheatPickupList[$] = ItemPickupBombBox;
410 cheatPickupList[$] = ItemPickupPaste;
411 cheatPickupList[$] = ItemPickupRopePile;
412 cheatPickupList[$] = ItemPickupShellBox;
413 cheatPickupList[$] = ItemPickupAnkh;
414 cheatPickupList[$] = ItemPickupCape;
415 cheatPickupList[$] = ItemPickupJetpack;
416 cheatPickupList[$] = ItemPickupUdjatEye;
417 cheatPickupList[$] = ItemPickupCrown;
418 cheatPickupList[$] = ItemPickupKapala;
419 cheatPickupList[$] = ItemPickupParachute;
420 cheatPickupList[$] = ItemPickupCompass;
421 cheatPickupList[$] = ItemPickupSpectacles;
422 cheatPickupList[$] = ItemPickupGloves;
423 cheatPickupList[$] = ItemPickupMitt;
424 cheatPickupList[$] = ItemPickupJordans;
425 cheatPickupList[$] = ItemPickupSpringShoes;
426 cheatPickupList[$] = ItemPickupSpikeShoes;
427 cheatPickupList[$] = ItemPickupTeleporter;
431 final UIPane createCheatPickupsPane () {
432 if (!level.player) return none;
434 UIPane pane = SpawnObject(UIPane);
436 pane.sprStore = sprStore;
438 pane.width = 320*3-64;
439 pane.height = 240*3-64;
441 foreach (auto ipk; cheatPickupList) {
442 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
446 //optionsPaneOfs.x = 100;
447 //optionsPaneOfs.y = 50;
453 // ////////////////////////////////////////////////////////////////////////// //
454 transient int instantGhost;
456 final UIPane createCheatFlagsPane () {
457 UIPane pane = SpawnObject(UIPane);
458 pane.id = 'CheatFlags';
459 pane.sprStore = sprStore;
461 pane.width = 320*3-64;
462 pane.height = 240*3-64;
466 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
467 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
468 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
469 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
470 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
471 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
472 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
473 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
474 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
475 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
476 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
477 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
478 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
479 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
480 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
481 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
482 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
483 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
485 optionsPaneOfs.x = 100;
486 optionsPaneOfs.y = 50;
492 final UIPane createOptionsPane () {
493 UIPane pane = SpawnObject(UIPane);
495 pane.sprStore = sprStore;
497 pane.width = 320*3-64;
498 pane.height = 240*3-64;
502 //!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.");
505 UILabel.Create(pane, "VISUAL OPTIONS");
506 UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
507 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
508 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).");
509 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
510 auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
511 startfs.onValueChanged = delegate void (int newval) {
516 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).");
517 fsmode.names[$] = "REAL";
518 fsmode.names[$] = "SCALED";
519 fsmode.onValueChanged = delegate void (int newval) {
527 UILabel.Create(pane, "");
528 UILabel.Create(pane, "HUD OPTIONS");
529 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
530 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.");
531 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
534 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
538 UILabel.Create(pane, "");
539 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
540 //!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.");
541 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
542 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
543 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.");
544 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.");
545 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
546 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.");
547 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
550 UILabel.Create(pane, "");
551 UILabel.Create(pane, "GAMEPLAY OPTIONS");
552 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.");
553 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
554 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
555 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!");
556 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.");
557 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.");
558 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
559 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.");
560 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.");
561 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.");
562 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.");
563 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?");
564 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.");
565 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.");
566 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.");
567 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
568 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
569 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
570 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
571 rstl.names[$] = "RANDOM";
572 rstl.names[$] = "NORMAL";
573 rstl.names[$] = "BIZARRE";
576 UILabel.Create(pane, "");
577 UILabel.Create(pane, "WHIP OPTIONS");
578 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
579 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
580 whiptype.names[$] = "NORMAL";
581 whiptype.names[$] = "LONG";
582 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.");
585 UILabel.Create(pane, "");
586 UILabel.Create(pane, "PLAYER OPTIONS");
587 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
588 herotype.names[$] = "SPELUNKY GUY";
589 herotype.names[$] = "DAMSEL";
590 herotype.names[$] = "TUNNEL MAN";
593 UILabel.Create(pane, "");
594 UILabel.Create(pane, "CHEAT OPTIONS");
595 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.");
596 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
597 plrlit.names[$] = "NEVER";
598 plrlit.names[$] = "FORCED DARKNESS";
599 plrlit.names[$] = "ALWAYS";
600 UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
601 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'.");
602 rdark.names[$] = "NEVER";
603 rdark.names[$] = "DEFAULT";
604 rdark.names[$] = "ALWAYS";
605 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.");
607 rghost.getNameCB = delegate string (int val) {
608 if (val < 0) return "INSTANT";
609 if (val == 0) return "NEVER";
610 if (val < 120) return va("%d SEC", val);
611 if (val%60 == 0) return va("%d MIN", val/60);
612 if (val%60 == 30) return va("%d.5 MIN", val/60);
613 return va("%d MIN, %d SEC", val/60, val%60);
615 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.");
617 UILabel.Create(pane, "");
618 UILabel.Create(pane, "CHEAT START OPTIONS");
619 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.");
620 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
621 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
622 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
623 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
626 UILabel.Create(pane, "");
627 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
628 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
629 mm.names[$] = "SILENCE";
630 mm.names[$] = "RESTART";
631 mm.names[$] = "DON'T TOUCH";
633 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
634 //mm.names[$] = "SILENCE";
635 mm.names[$] = "RESTART";
636 mm.names[$] = "DON'T TOUCH";
639 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
641 swstereo.onValueChanged = delegate void (int newval) {
642 SoundSystem.SwapStereo = newval;
646 UILabel.Create(pane, "");
647 UILabel.Create(pane, "SOUND CONTROL CENTER");
648 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
649 rmusonoff.onValueChanged = delegate void (int newval) {
650 global.restartMusic();
653 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
655 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
656 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
658 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
659 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
662 saveOptionsDG = delegate void () {
663 writeln("saving options");
666 optionsPaneOfs.x = 42;
667 optionsPaneOfs.y = 0;
673 final void createBindingsControl (UIPane pane, int keyidx) {
676 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
677 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
678 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
679 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
680 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
681 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
682 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
683 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
684 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
685 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
686 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
689 int arridx = GameConfig.getKeyIndex(keyidx);
690 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
694 final UIPane createBindingsPane () {
695 UIPane pane = SpawnObject(UIPane);
696 pane.id = 'KeyBindings';
697 pane.sprStore = sprStore;
699 pane.width = 320*3-64;
700 pane.height = 240*3-64;
702 createBindingsControl(pane, GameConfig::Key.Left);
703 createBindingsControl(pane, GameConfig::Key.Right);
704 createBindingsControl(pane, GameConfig::Key.Up);
705 createBindingsControl(pane, GameConfig::Key.Down);
706 createBindingsControl(pane, GameConfig::Key.Jump);
707 createBindingsControl(pane, GameConfig::Key.Run);
708 createBindingsControl(pane, GameConfig::Key.Attack);
709 createBindingsControl(pane, GameConfig::Key.Switch);
710 createBindingsControl(pane, GameConfig::Key.Pay);
711 createBindingsControl(pane, GameConfig::Key.Bomb);
712 createBindingsControl(pane, GameConfig::Key.Rope);
714 saveOptionsDG = delegate void () {
715 writeln("saving keys");
716 saveKeyboardBindings();
718 optionsPaneOfs.x = 120;
719 optionsPaneOfs.y = 140;
725 // ////////////////////////////////////////////////////////////////////////// //
726 void clearGameMovement () {
727 debugMovement = SpawnObject(DebugSessionMovement);
728 debugMovement.playconfig = SpawnObject(GameConfig);
729 debugMovement.playconfig.copyGameplayConfigFrom(config);
730 debugMovement.resetReplay();
734 void saveGameMovement (string fname, optional bool packit) {
735 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
736 saveMovementLastTime = GetTickCount();
740 void loadGameMovement (string fname) {
741 delete debugMovement;
742 debugMovement = appLoadOptions(DebugSessionMovement, fname);
743 debugMovement.resetReplay();
746 origStats = level.stats;
747 origStats.global = none;
748 level.stats = SpawnObject(GameStats);
749 level.stats.global = global;
752 config = debugMovement.playconfig;
753 global.config = config;
754 origRoomSeed = global.globalRoomSeed;
755 origOtherSeed = global.globalOtherSeed;
756 writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
761 void stopReplaying () {
763 writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
764 global.globalRoomSeed = origRoomSeed;
765 global.globalOtherSeed = origOtherSeed;
767 delete debugMovement;
768 saveGameSession = false;
769 replayGameSession = false;
770 doGameSavingPlaying = Replay.None;
773 origStats.global = global;
774 level.stats = origStats;
780 global.config = origConfig;
786 // ////////////////////////////////////////////////////////////////////////// //
787 final bool saveGame (string gmname) {
788 return appSaveOptions(level, gmname);
792 final bool loadGame (string gmname) {
793 auto olddel = ImmediateDelete;
794 ImmediateDelete = false;
796 auto stats = level.stats;
799 auto lvl = appLoadOptions(GameLevel, gmname);
801 //lvl.global.config = config;
806 global = level.global;
807 global.config = config;
809 level.sprStore = sprStore;
810 level.bgtileStore = bgtileStore;
813 level.onBeforeFrame = &beforeNewFrame;
814 level.onAfterFrame = &afterNewFrame;
815 level.onInterFrame = &interFrame;
816 level.onLevelExitedCB = &levelExited;
817 level.onCameraTeleported = &cameraTeleportedCB;
819 //level.viewWidth = Video.screenWidth;
820 //level.viewHeight = Video.screenHeight;
821 level.viewWidth = 320*3;
822 level.viewHeight = 240*3;
825 level.centerViewAtPlayer();
826 teleportCameraAt(level.viewStart);
828 recalcCameraCoords(0);
833 level.stats.global = level.global;
835 ImmediateDelete = olddel;
836 CollectGarbage(true); // destroy delayed objects too
841 // ////////////////////////////////////////////////////////////////////////// //
842 float lastThinkerTime;
843 int replaySkipFrame = 0;
846 final void onTimePasses () {
847 float curTime = GetTickCount();
848 if (lastThinkerTime > 0) {
849 if (curTime < lastThinkerTime) {
850 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
851 lastThinkerTime = curTime;
854 if (replayFastForward && replaySkipFrame) {
856 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
859 level.processThinkers(curTime-lastThinkerTime);
861 lastThinkerTime = curTime;
865 final void resetFramesAndForceOne () {
866 float curTime = GetTickCount();
867 lastThinkerTime = curTime;
869 auto wasPaused = level.gamePaused;
870 level.gamePaused = false;
871 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
872 level.processThinkers(GameLevel::FrameTime);
873 level.gamePaused = wasPaused;
874 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
878 // ////////////////////////////////////////////////////////////////////////// //
879 private float currFrameDelta; // so level renderer can properly interpolate the player
880 private GameLevel::IVec2D camPrev, camCurr;
881 private GameLevel::IVec2D camShake;
882 private GameLevel::IVec2D viewCameraPos;
885 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
890 viewCameraPos.x = pos.x;
891 viewCameraPos.y = pos.y;
897 // call `recalcCameraCoords()` to get real camera coords after this
898 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
899 // check if camera is moved too far, and teleport it
901 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
902 abs(camCurr.y-pos.y)/global.scale >= 16*4))
904 teleportCameraAt(pos);
906 camPrev.x = camCurr.x;
907 camPrev.y = camCurr.y;
911 camShake.x = level.shakeDir.x*global.scale;
912 camShake.y = level.shakeDir.y*global.scale;
916 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
917 currFrameDelta = frameDelta;
918 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
919 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
921 viewCameraPos.x += camShake.x;
922 viewCameraPos.y += camShake.y;
926 GameLevel::SavedKeyState savedKeyState;
928 final void pauseGame () {
929 if (!level.gamePaused) {
930 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
931 level.gamePaused = true;
932 global.pauseAllSounds();
937 final void unpauseGame () {
938 if (level.gamePaused) {
939 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
940 level.gamePaused = false;
941 level.gameShowHelp = false;
942 level.gameHelpScreen = 0;
943 //lastThinkerTime = 0;
944 global.resumeAllSounds();
946 pauseRequested = false;
947 helpRequested = false;
952 final void beforeNewFrame (bool frameSkip) {
955 level.disablePlayerThink = true;
958 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
959 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
960 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
962 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
963 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
964 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
965 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
967 level.disablePlayerThink = false;
973 if (!level.gamePaused) {
974 // save seeds for afterframe processing
976 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
977 debugMovement.otherSeed = global.globalOtherSeed;
978 debugMovement.roomSeed = global.globalRoomSeed;
982 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
984 #ifdef BIGGER_REPLAY_DATA
985 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
986 debugMovement.keypresses.length += 1;
987 level.keysSaveState(debugMovement.keypresses[$-1]);
988 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
989 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
993 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
994 #ifdef BIGGER_REPLAY_DATA
995 if (debugMovement.keypos < debugMovement.keypresses.length) {
996 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
997 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
998 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
999 ++debugMovement.keypos;
1005 auto code = debugMovement.getKey(out kbidx, out down);
1006 if (code == DebugSessionMovement::END_OF_RECORD) {
1007 // do this in main loop, so we can view totals
1011 if (code == DebugSessionMovement::END_OF_FRAME) {
1014 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1015 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1023 final void afterNewFrame (bool frameSkip) {
1024 if (!replayFastForward) replaySkipFrame = 0;
1026 if (level.gamePaused) return;
1028 if (!level.gamePaused) {
1029 if (doGameSavingPlaying != Replay.None) {
1030 if (doGameSavingPlaying == Replay.Saving) {
1031 replayFastForward = false; // just in case
1032 #ifndef BIGGER_REPLAY_DATA
1033 debugMovement.addEndOfFrame();
1035 auto stt = GetTickCount();
1036 if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
1037 } else if (doGameSavingPlaying == Replay.Replaying) {
1038 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1039 replaySkipFrame = 1;
1045 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1046 //SoundSystem.UpdateSounds();
1048 //if (!freeRide) level.fixCamera();
1049 setNewCameraPos(level.viewStart);
1051 prevCameraX = currCameraX;
1052 prevCameraY = currCameraY;
1053 currCameraX = level.cameraX;
1054 currCameraY = level.cameraY;
1055 // disable camera interpolation if the screen is shaking
1056 if (level.shakeX|level.shakeY) {
1057 prevCameraX = currCameraX;
1058 prevCameraY = currCameraY;
1061 // disable camera interpolation if it moves too far away
1062 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1063 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1065 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1067 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1068 pauseRequested = false;
1070 if (helpRequested) {
1071 helpRequested = false;
1072 level.gameShowHelp = true;
1073 level.gameHelpScreen = 0;
1076 if (!showHelp) showHelp = true;
1083 final void interFrame (float frameDelta) {
1084 if (!config.interpolateMovement) return;
1085 recalcCameraCoords(frameDelta);
1089 final void cameraTeleportedCB () {
1090 teleportCameraAt(level.viewStart);
1091 recalcCameraCoords(0);
1095 // ////////////////////////////////////////////////////////////////////////// //
1097 final void setColorByIdx (bool isset, int col) {
1099 // missed collision: red
1100 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1101 } else if (col == -999) {
1102 // superfluous collision: blue
1103 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1104 } else if (col <= 0) {
1105 // no collision: yellow
1106 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1107 } else if (col > 0) {
1109 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1114 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1116 CollisionMask cm = CollisionMask.Create(frm, false);
1118 int scale = global.config.scale;
1119 int bx0, by0, bx1, by1;
1120 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1121 Video.color = 0x7f_00_00_ff;
1122 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1123 if (!cm.isEmptyMask) {
1124 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1125 foreach (int iy; 0..cm.height) {
1126 foreach (int ix; 0..cm.width) {
1127 int v = cm.mask[ix, iy];
1128 foreach (int dx; 0..32) {
1131 Video.color = 0x3f_00_ff_00;
1132 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1141 foreach (int iy; 0..frm.tex.height) {
1142 foreach (int ix; 0..(frm.tex.width+31)/31) {
1143 foreach (int dx; 0..32) {
1145 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1146 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1147 setColorByIdx(true, col);
1148 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1150 Video.color = 0xaf_00_ff_00;
1152 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1158 if (frm.bw > 0 && frm.bh > 0) {
1159 setColorByIdx(true, col);
1160 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1161 Video.color = 0xff_00_00;
1162 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1171 // ////////////////////////////////////////////////////////////////////////// //
1172 transient int drawStats;
1173 transient array!int statsTopItem;
1176 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1177 auto sa = string(a.objName).toUpperCase;
1178 auto sb = string(b.objName).toUpperCase;
1183 final int getStatsTopItem () {
1184 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1188 final void setStatsTopItem (int val) {
1189 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1190 statsTopItem[drawStats] = val;
1194 final void resetStatsTopItem () {
1199 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1200 sprStore.loadFont('sFontSmall');
1206 final int calcStatsVisItems () {
1209 statsDrawGetStartPosLoadFont(currX, currY);
1210 int endY = level.viewHeight-(currY*2);
1211 return max(1, endY/sprStore.getFontHeight(scale));
1215 int getStatsItemCount () {
1216 switch (drawStats) {
1217 case 2: return level.stats.totalKills.length;
1218 case 3: return level.stats.totalDeaths.length;
1219 case 4: return level.stats.totalCollected.length;
1225 final void statsMoveUp () {
1226 int count = getStatsItemCount();
1227 if (count < 0) return;
1228 int visItems = calcStatsVisItems();
1229 if (count <= visItems) { resetStatsTopItem(); return; }
1230 int top = getStatsTopItem();
1232 setStatsTopItem(top-1);
1236 final void statsMoveDown () {
1237 int count = getStatsItemCount();
1238 if (count < 0) return;
1239 int visItems = calcStatsVisItems();
1240 if (count <= visItems) { resetStatsTopItem(); return; }
1241 int top = getStatsTopItem();
1242 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1243 top = clamp(top+1, 0, count-visItems);
1244 setStatsTopItem(top);
1248 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1249 arr.sort(&totalsNameCmpCB);
1253 statsDrawGetStartPosLoadFont(currX, currY);
1255 int endY = level.viewHeight-(currY*2);
1256 int visItems = calcStatsVisItems();
1258 if (arr.length <= visItems) resetStatsTopItem();
1260 int topItem = getStatsTopItem();
1264 Video.color = 0x3f_ff_ff_00;
1265 auto spr = sprStore['sPageUp'];
1266 spr.frames[0].tex.blitAt(currX-28, currY, scale);
1269 // "downscroll" mark
1270 if (topItem+visItems < arr.length) {
1271 Video.color = 0x3f_ff_ff_00;
1272 auto spr = sprStore['sPageDown'];
1273 spr.frames[0].tex.blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1276 Video.color = 0xff_ff_00;
1277 int hiColor = 0x00_ff_00;
1278 int hiColor1 = 0xf_ff_ff;
1281 while (it < arr.length && visItems-- > 0) {
1282 sprStore.renderTextWithHighlight(currX, currY, va("%s |%s| ~%d~ TIME%s", pfx, string(arr[it].objName).toUpperCase, arr[it].count, (arr[it].count != 1 ? "S" : "")), scale, hiColor, hiColor1);
1283 currY += sprStore.getFontHeight(scale);
1289 void drawStatsScreen () {
1290 int deathCount, killCount, collectCount;
1292 sprStore.loadFont('sFontSmall');
1294 Video.color = 0xff_ff_ff;
1295 level.drawTextAtS3Centered(240-2-8, "ESC-RETURN F10-QUIT CTRL+DEL-SUICIDE");
1296 level.drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
1298 Video.color = 0xff_ff_00;
1299 int hiColor = 0x00_ff_00;
1301 switch (drawStats) {
1302 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1303 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1304 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1307 if (drawStats > 1) {
1309 foreach (ref auto i; statsTopItem) i = 0;
1314 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1315 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1316 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1322 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1323 currY += sprStore.getFontHeight(scale);
1325 int gw = level.stats.gamesWon;
1326 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1327 currY += sprStore.getFontHeight(scale);
1329 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1330 currY += sprStore.getFontHeight(scale);
1332 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1333 currY += sprStore.getFontHeight(scale);
1335 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1336 currY += sprStore.getFontHeight(scale);
1338 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1339 currY += sprStore.getFontHeight(scale);
1341 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1342 currY += sprStore.getFontHeight(scale);
1344 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1345 currY += sprStore.getFontHeight(scale);
1347 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1348 currY += sprStore.getFontHeight(scale);
1350 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1351 currY += sprStore.getFontHeight(scale);
1353 int gs = level.stats.totalGhostSummoned;
1354 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1355 currY += sprStore.getFontHeight(scale);
1357 currY += sprStore.getFontHeight(scale);
1358 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1359 currY += sprStore.getFontHeight(scale);
1364 if (Video.frameTime == 0) {
1366 Video.requestRefresh();
1371 if (level.framesProcessedFromLastClear < 1) return;
1372 calcMouseMapCoords();
1374 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1375 Video.clearScreen();
1376 Video.stencil = false;
1377 Video.color = 0xff_ff_ff;
1378 Video.textureFiltering = false;
1379 // don't touch framebuffer alpha
1380 Video.colorMask = Video::CMask.Colors;
1382 Video::ScissorRect scsave;
1383 bool doRestoreGL = false;
1386 if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1388 Video.getScissor(scsave);
1389 Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1390 Video.glPushMatrix();
1391 Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1395 if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1397 float scx = float(Video.screenWidth)/float(level.viewWidth);
1398 float scy = float(Video.screenHeight)/float(level.viewHeight);
1399 float scale = fmin(scx, scy);
1400 int calcedW = trunc(level.viewWidth*scale);
1401 int calcedH = trunc(level.viewHeight*scale);
1402 Video.getScissor(scsave);
1403 int ofsx = (Video.screenWidth-calcedW)/2;
1404 int ofsy = (Video.screenHeight-calcedH)/2;
1405 Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1406 Video.glPushMatrix();
1407 Video.glTranslate(ofsx, ofsy);
1408 Video.glScale(scale, scale);
1411 //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1412 //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1416 level.viewOffsetX = 0;
1417 level.viewOffsetY = 0;
1418 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1421 float scx = float(Video.screenWidth)/float(level.viewWidth);
1422 float scy = float(Video.screenHeight)/float(level.viewHeight);
1423 Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1428 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1430 if (level.gamePaused && showHelp != 2) {
1431 if (mouseLevelX != int.min) {
1432 int scale = level.global.scale;
1433 if (renderMouseRect) {
1434 Video.color = 0xcf_ff_ff_00;
1435 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1437 if (renderMouseTile) {
1438 Video.color = 0xaf_ff_00_00;
1439 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1444 switch (doGameSavingPlaying) {
1446 Video.color = 0x7f_00_ff_00;
1447 sprStore.loadFont('sFont');
1448 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1450 case Replay.Replaying:
1451 if (level.player && !level.player.dead) {
1452 Video.color = 0x7f_ff_00_00;
1453 sprStore.loadFont('sFont');
1454 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1455 int th = sprStore.getFontHeight(2);
1456 if (replayFastForward) {
1457 sprStore.loadFont('sFontSmall');
1458 string sstr = va("x%d", replayFastForwardSpeed+1);
1459 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1464 if (saveGameSession) {
1465 Video.color = 0x7f_ff_7f_00;
1466 sprStore.loadFont('sFont');
1467 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1473 if (level.player && level.player.dead && !showHelp) {
1475 Video.color = 0x8f_00_00_00;
1476 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1481 if (true /*level.inWinCutscene == 0*/) {
1482 Video.color = 0xff_ff_ff;
1483 sprStore.loadFont('sFontSmall');
1484 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1486 "PRESS $PAY TO RESTART GAME\n"~
1488 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1490 "TOTAL PLAYING TIME: |%s|"~
1492 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1493 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1494 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1496 GameLevel.time2str(level.stats.playingTime)
1498 kmsg = global.expandString(kmsg);
1499 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1506 Video.color = 0xff_7f_00;
1507 sprStore.loadFont('sFontSmall');
1508 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1509 auto spf = smask.frames[maskFrame];
1510 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1512 spf.bx, spf.by, spf.bw, spf.bh,
1513 (spf.maskEmpty ? "TAN" : "ONA"),
1514 (spf.precise ? "TAN" : "ONA")),
1517 //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1518 //writeln("pos=(", maskSX, ",", maskSY, ")");
1519 int scale = global.config.scale;
1520 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1521 int mapX = xofs/scale+maskSX;
1522 int mapY = yofs/scale+maskSY;
1525 writeln("==== tiles ====");
1527 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1528 if (t.spectral || !t.isInstanceAlive) return false;
1529 Video.color = 0x7f_ff_00_00;
1530 Video.fillRect(t.x0*global.config.scale-viewCameraPos.x, t.y0*global.config.scale-viewCameraPos.y, t.width*global.config.scale, t.height*global.config.scale);
1531 auto tsf = t.getSpriteFrame();
1533 auto spf = smask.frames[maskFrame];
1534 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1535 int mapX = xofs/global.config.scale+maskSX;
1536 int mapY = yofs/global.config.scale+maskSY;
1539 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1540 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1541 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1545 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1546 Video.color = 0x7f_ff_00_00;
1547 Video.fillRect(t.x0*global.config.scale-viewCameraPos.x, t.y0*global.config.scale-viewCameraPos.y, t.width*global.config.scale, t.height*global.config.scale);
1551 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1553 Video.color = 0xaf_ff_ff_ff;
1554 spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1555 Video.color = 0xff_ff_00;
1556 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1560 int fx0, fy0, fx1, fy1;
1561 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1562 Video.color = 0x7f_00_00_ff;
1563 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1569 Video.color = 0x8f_00_00_00;
1570 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1572 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1577 Video.color = 0xff_ff_00;
1578 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1579 if (showHelp == 1) {
1580 sprStore.loadFont('sFontSmall');
1581 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1582 "F1: show this help\n"~
1584 "K : redefine keys\n"~
1585 "I : toggle interpolaion\n"~
1586 "N : create some blood\n"~
1587 "R : generate a new level\n"~
1588 "F : toggle \"Frozen Area\"\n"~
1589 "X : resurrect player\n"~
1590 "Q : teleport to exit\n"~
1591 "D : teleport to damel\n"~
1593 "C : cheat flags menu\n"~
1594 "P : cheat pickup menu\n"~
1595 "E : cheat enemy menu\n"~
1596 "Enter: cheat items menu\n"~
1598 "TAB: toggle 'freeroam' mode\n"~
1602 if (level) level.renderPauseOverlay();
1606 //SoundSystem.UpdateSounds();
1608 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1611 Video.setScissor(scsave);
1612 Video.glPopMatrix();
1617 Video.color = 0xaf_ff_ff_ff;
1618 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1623 // ////////////////////////////////////////////////////////////////////////// //
1624 transient bool gameJustOver;
1625 transient bool waitingForPayRestart;
1628 final void calcMouseMapCoords () {
1629 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1630 mouseLevelX = int.min;
1631 mouseLevelY = int.min;
1634 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1635 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1636 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1640 final void onEvent (ref event_t evt) {
1641 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1643 if (evt.type == ev_winfocus) {
1644 if (level && !evt.focused) {
1649 //writeln("FOCUS!");
1650 Video.getMousePos(out mouseX, out mouseY);
1655 if (evt.type == ev_mouse) {
1658 calcMouseMapCoords();
1661 if (evt.type == ev_keydown && evt.keycode == K_F12) {
1662 if (level) toggleFullscreen();
1666 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1667 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1668 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1671 if (evt.type == ev_keydown) {
1672 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1673 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1674 renderMouseTile = evt.bCtrl;
1675 renderMouseRect = evt.bAlt;
1678 if (evt.type == ev_keyup) {
1679 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1680 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1681 renderMouseTile = evt.bCtrl;
1682 renderMouseRect = evt.bAlt;
1685 if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1687 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1688 int newScale = evt.keycode-48;
1689 if (global.config.scale != newScale) {
1690 global.config.scale = newScale;
1693 cameraTeleportedCB();
1700 if (evt.type == ev_mouse) {
1701 maskSX = evt.x/global.config.scale;
1702 maskSY = evt.y/global.config.scale;
1705 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1706 maskFrame = max(0, maskFrame-1);
1709 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1710 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1719 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1721 if (saveOptionsDG) saveOptionsDG();
1722 saveOptionsDG = none;
1724 //SoundSystem.UpdateSounds(); // just in case
1725 if (global.hasSpectacles) level.pickedSpectacles();
1728 optionsPane.onEvent(evt);
1732 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1733 if (evt.type == ev_keydown) {
1734 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1735 switch (evt.keycode) {
1736 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1737 case K_F2: if (showHelp != 2) unpauseGame(); return;
1738 case K_F10: Video.requestQuit(); return;
1739 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1741 case K_UPARROW: case K_PAD8:
1742 if (drawStats) statsMoveUp();
1745 case K_DOWNARROW: case K_PAD2:
1746 if (drawStats) statsMoveDown();
1749 case K_LEFTARROW: case K_PAD4:
1750 if (level && showHelp == 2 && level.gameShowHelp) {
1751 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1755 case K_RIGHTARROW: case K_PAD6:
1756 if (level && showHelp == 2 && level.gameShowHelp) {
1757 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1771 resetFramesAndForceOne();
1777 if (/*evt.bCtrl &&*/ showHelp != 2) {
1787 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1788 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1789 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1790 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1791 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1792 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1793 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1794 //case K_j: global.hasJordans = !global.hasJordans; return;
1796 if (/*evt.bCtrl &&*/ showHelp != 2) {
1797 level.resurrectPlayer();
1802 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1803 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1804 if (evt.bAlt && level.player && level.player.dead) {
1805 saveGameSession = false;
1806 replayGameSession = true;
1810 if (/*evt.bCtrl &&*/ showHelp != 2) {
1811 if (evt.bShift) global.idol = false;
1812 level.generateLevel();
1813 level.centerViewAtPlayer();
1814 teleportCameraAt(level.viewStart);
1815 resetFramesAndForceOne();
1819 global.toggleMusic();
1822 if (/*evt.bCtrl &&*/ showHelp != 2) {
1823 if (level.allExits.length) {
1824 level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1830 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1831 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1833 level.teleportPlayerTo(damsel.ix, damsel.iy);
1839 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1843 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1846 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1849 level.teleportPlayerTo(obj.ix, obj.iy-4);
1855 if (/*evt.bCtrl &&*/ showHelp != 2) {
1856 if (level && mouseLevelX != int.min) {
1857 int scale = level.global.scale;
1858 int mapX = mouseLevelX;
1859 int mapY = mouseLevelY;
1860 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1866 if (level && mouseLevelX != int.min) {
1867 int scale = level.global.scale;
1868 int mapX = mouseLevelX;
1869 int mapY = mouseLevelY;
1872 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1873 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1874 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1878 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1879 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1885 if (/*evt.bAlt &&*/ showHelp != 2) {
1886 auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1887 //if (obj) obj.monkey = monkey;
1889 //playSound('sndThump');
1895 case K_DELETE: // suicide
1896 if (doGameSavingPlaying == Replay.None) {
1897 if (level.player && !level.player.dead && evt.bCtrl) {
1898 global.hasAnkh = false;
1899 level.global.plife = 1;
1900 level.player.invincible = 0;
1901 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1902 if (xplo) xplo.suicide = true;
1909 if (level.player && !level.player.dead && evt.bAlt) {
1910 if (doGameSavingPlaying != Replay.None) {
1911 if (doGameSavingPlaying == Replay.Replaying) {
1913 } else if (doGameSavingPlaying == Replay.Saving) {
1914 saveGameMovement(dbgSessionMovementFileName, packit:true);
1916 doGameSavingPlaying = Replay.None;
1918 saveGameSession = false;
1919 replayGameSession = false;
1926 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1927 level.stats.setMoneyCheat();
1928 level.stats.addMoney(10000);
1934 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1935 if (level.player && level.player.dead) {
1936 //Video.requestQuit();
1938 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1940 #ifdef QUIT_DOUBLE_ESC
1941 if (++escCount == 2) Video.requestQuit();
1944 pauseRequested = true;
1950 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
1951 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1952 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1955 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1958 if (!level.player || !level.player.dead) {
1959 gameJustOver = false;
1960 } else if (level.player && level.player.dead) {
1961 if (!gameJustOver) {
1963 gameJustOver = true;
1964 waitingForPayRestart = true;
1965 level.clearKeysPressRelease();
1966 if (doGameSavingPlaying == Replay.None) {
1967 stopReplaying(); // just in case
1971 replayFastForward = false;
1972 if (doGameSavingPlaying == Replay.Saving) {
1973 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
1974 doGameSavingPlaying = Replay.None;
1975 //clearGameMovement();
1976 saveGameSession = false;
1977 replayGameSession = false;
1980 if (evt.type == ev_keydown || evt.type == ev_keyup) {
1981 bool down = (evt.type == ev_keydown);
1982 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
1983 if (down && evt.keycode == K_f) {
1985 if (replayFastForwardSpeed != 4) {
1986 replayFastForwardSpeed = 4;
1987 replayFastForward = true;
1989 replayFastForward = !replayFastForward;
1992 replayFastForwardSpeed = 2;
1993 replayFastForward = !replayFastForward;
1997 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
1998 foreach (int kbidx, int kval; global.config.keybinds) {
1999 if (kval && kval == evt.keycode) {
2000 #ifndef BIGGER_REPLAY_DATA
2001 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2003 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2007 if (level.player && level.player.dead) {
2008 if (down && evt.keycode == K_r && evt.bAlt) {
2009 saveGameSession = false;
2010 replayGameSession = true;
2013 if (down && evt.keycode == K_s && evt.bAlt) {
2014 bool wasSaveReq = saveGameSession;
2015 stopReplaying(); // just in case
2016 saveGameSession = !wasSaveReq;
2017 replayGameSession = false;
2020 if (replayGameSession) {
2021 stopReplaying(); // just in case
2022 saveGameSession = false;
2023 replayGameSession = false;
2024 loadGameMovement(dbgSessionMovementFileName);
2025 loadGame(dbgSessionStateFileName);
2026 doGameSavingPlaying = Replay.Replaying;
2028 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2029 if (waitingForPayRestart) {
2030 level.isKeyReleased(GameConfig::Key.Pay);
2031 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2033 level.isKeyPressed(GameConfig::Key.Pay);
2034 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2035 auto doSave = saveGameSession;
2036 stopReplaying(); // just in case
2037 level.clearKeysPressRelease();
2038 level.restartGame();
2039 level.generateNormalLevel();
2041 saveGameSession = false;
2042 replayGameSession = false;
2043 writeln("DBG: saving game session...");
2044 clearGameMovement();
2045 doGameSavingPlaying = Replay.Saving;
2046 saveGame(dbgSessionStateFileName);
2047 //saveGameMovement(dbgSessionMovementFileName);
2058 void levelExited () {
2064 void initializeVideo () {
2065 Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2066 if (Video.realStencilBits < 8) {
2067 Video.closeScreen();
2068 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2070 if (!Video.framebufferHasAlpha) {
2071 Video.closeScreen();
2072 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!");
2077 void toggleFullscreen () {
2078 Video.closeScreen();
2079 fullscreen = !fullscreen;
2084 final void runGameLoop () {
2085 Video.frameTime = 0; // unlimited FPS
2086 lastThinkerTime = 0;
2088 sprStore = SpawnObject(SpriteStore);
2089 sprStore.bDumpLoaded = false;
2091 bgtileStore = SpawnObject(BackTileStore);
2092 bgtileStore.bDumpLoaded = false;
2094 level = SpawnObject(GameLevel);
2095 level.setup(global, sprStore, bgtileStore);
2097 level.BuildYear = BuildYear;
2098 level.BuildMonth = BuildMonth;
2099 level.BuildDay = BuildDay;
2100 level.BuildHour = BuildHour;
2101 level.BuildMin = BuildMin;
2103 level.global = global;
2104 level.sprStore = sprStore;
2105 level.bgtileStore = bgtileStore;
2108 //level.stats.introViewed = 0;
2110 if (level.stats.introViewed == 0) {
2111 startMode = StartMode.Intro;
2112 writeln("FORCED INTRO");
2114 //writeln("INTRO VIWED: ", level.stats.introViewed);
2115 if (level.global.config.skipIntro) startMode = StartMode.Title;
2118 level.onBeforeFrame = &beforeNewFrame;
2119 level.onAfterFrame = &afterNewFrame;
2120 level.onInterFrame = &interFrame;
2121 level.onLevelExitedCB = &levelExited;
2122 level.onCameraTeleported = &cameraTeleportedCB;
2125 maskSX = -0x0ff_fff;
2127 smask = sprStore['sExplosionMask'];
2131 sprStore.loadFont('sFontSmall');
2133 level.viewWidth = 320*3;
2134 level.viewHeight = 240*3;
2136 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2137 //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2138 fullscreen = global.config.startFullscreen;
2141 //SoundSystem.SwapStereo = config.swapStereo;
2142 SoundSystem.NumChannels = 32;
2143 SoundSystem.MaxHearingDistance = 12000;
2144 //SoundSystem.DopplerFactor = 1.0f;
2145 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2146 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2147 SoundSystem.ReferenceDistance = 16.0f*4;
2148 SoundSystem.MaxDistance = 16.0f*(5*10);
2150 SoundSystem.Initialize();
2151 if (!SoundSystem.IsInitialized) {
2152 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2153 global.soundDisabled = true;
2154 global.musicDisabled = true;
2156 global.fixVolumes();
2158 level.restartGame(); // this will NOT generate a new level
2163 texTigerEye = GLTexture.Load("sprites/teye0.png");
2165 if (global.cheatEndGameSequence) {
2166 level.winTime = 12*60+42;
2167 level.stats.money = 6666;
2168 switch (global.cheatEndGameSequence) {
2169 case 1: default: level.startWinCutscene(); break;
2170 case 2: level.startWinCutsceneVolcano(); break;
2171 case 3: level.startWinCutsceneWinFall(); break;
2174 switch (startMode) {
2175 case StartMode.Title: level.restartTitle(); break;
2176 case StartMode.Intro: level.restartIntro(); break;
2177 case StartMode.Stars: level.restartStarsRoom(); break;
2178 case StartMode.Sun: level.restartSunRoom(); break;
2179 case StartMode.Moon: level.restartMoonRoom(); break;
2181 level.generateNormalLevel();
2182 if (startMode == StartMode.Dead) {
2183 level.player.dead = true;
2184 level.player.visible = false;
2190 //global.rope = 666;
2191 //global.bombs = 666;
2193 //global.globalRoomSeed = 871520037;
2194 //global.globalOtherSeed = 1047036290;
2196 //level.createTitleRoom();
2197 //level.createTrans4Room();
2198 //level.createOlmecRoom();
2199 //level.generateLevel();
2201 //level.centerViewAtPlayer();
2202 teleportCameraAt(level.viewStart);
2203 //writeln(Video.swapInterval);
2205 Video.runEventLoop();
2206 Video.closeScreen();
2207 SoundSystem.Shutdown();
2209 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2217 // ////////////////////////////////////////////////////////////////////////// //
2218 // duplicates are not allowed!
2219 final void checkGameObjNames () {
2220 array!(class!Object) known;
2222 int classCount = 0, namedCount = 0;
2223 foreach AllClasses(Object, out cc) {
2224 auto gn = GetClassGameObjName(cc);
2226 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2227 auto nid = NameToInt(gn);
2228 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));
2234 writeln(classCount, " classes, ", namedCount, " game object classes.");
2238 // ////////////////////////////////////////////////////////////////////////// //
2239 #include "timelimit.vc"
2240 //const int TimeLimitDate = 2018232;
2243 void performTimeCheck () {
2244 #ifdef DISABLE_TIME_CHECK
2246 if (TigerEye) return;
2249 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2252 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2254 int tldate = tm.year*1000+tm.yday;
2256 if (tldate > TimeLimitDate) {
2257 level.maxPlayingTime = 24;
2259 //writeln("*** days left: ", TimeLimitDate-tldate);
2265 void setupCheats () {
2268 startMode = StartMode.Alive;
2269 global.currLevel = 2;
2270 global.scumGenShop = true;
2271 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2272 global.scumGenShopType = GameGlobal::ShopType.Craps;
2273 //global.config.scale = 1;
2276 //startMode = StartMode.Intro;
2279 global.currLevel = 2;
2280 startMode = StartMode.Alive;
2283 global.currLevel = 5;
2284 startMode = StartMode.Alive;
2285 global.scumGenLake = true;
2286 global.config.scale = 1;
2289 startMode = StartMode.Alive;
2290 global.cheatCanSkipOlmec = true;
2291 global.currLevel = 16;
2292 //global.currLevel = 5;
2293 //global.currLevel = 13;
2294 //global.config.scale = 1;
2296 //startMode = StartMode.Dead;
2297 //startMode = StartMode.Title;
2298 //startMode = StartMode.Stars;
2299 //startMode = StartMode.Sun;
2300 startMode = StartMode.Moon;
2302 //global.scumGenSacrificePit = true;
2303 //global.scumAlwaysSacrificeAltar = true;
2305 // first lush jungle level
2306 //global.levelType = 1;
2308 global.scumGenCemetary = true;
2310 //global.idol = false;
2311 //global.currLevel = 5;
2313 //global.isTunnelMan = true;
2316 //global.currLevel = 5;
2317 //global.scumGenLake = true;
2319 //global.currLevel = 5;
2320 //global.currLevel = 9;
2321 //global.currLevel = 13;
2322 //global.currLevel = 14;
2323 //global.cheatEndGameSequence = 1;
2326 //global.currLevel = 6;
2327 global.scumGenAlienCraft = true;
2328 global.currLevel = 9;
2329 //global.scumGenYetiLair = true;
2330 //global.genBlackMarket = true;
2331 //startDead = false;
2332 startMode = StartMode.Alive;
2335 global.cheatCanSkipOlmec = true;
2336 global.currLevel = 15;
2337 startMode = StartMode.Alive;
2340 global.scumGenShop = true;
2341 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2342 global.scumGenShopType = GameGlobal::ShopType.Craps;
2343 //global.scumGenShopType = 6; // craps
2344 //global.scumGenShopType = 7; // kissing
2346 //global.scumAlwaysSacrificeAltar = true;
2350 void setupSeeds () {
2354 // ////////////////////////////////////////////////////////////////////////// //
2356 checkGameObjNames();
2358 appSetName("k8spelunky");
2359 config = SpawnObject(GameConfig);
2360 global = SpawnObject(GameGlobal);
2361 global.config = config;
2362 config.heroType = GameConfig::Hero.Spelunker;
2364 global.randomizeSeedAll();
2366 fillCheatPickupList();
2367 fillCheatItemsList();
2368 fillCheatEnemiesList();
2371 loadKeyboardBindings();