1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
11 * Spelunky is distributed in the hope that it will be entertaining and useful,
12 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
14 * The Spelunky User License should be available in "Game Information", which
15 * can be found in the Resource Explorer, or as an external file called COPYING.
16 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
18 **********************************************************************************/
26 //#define QUIT_DOUBLE_ESC
30 //#define BIGGER_REPLAY_DATA
32 // ////////////////////////////////////////////////////////////////////////// //
33 #include "mapent/0all.vc"
34 #include "PlayerPawn.vc"
35 #include "PlayerPowerup.vc"
36 #include "GameLevel.vc"
39 // ////////////////////////////////////////////////////////////////////////// //
40 #include "uisimple.vc"
43 // ////////////////////////////////////////////////////////////////////////// //
44 class DebugSessionMovement : Object;
46 #ifdef BIGGER_REPLAY_DATA
47 array!(GameLevel::SavedKeyState) keypresses;
49 array!ubyte keypresses; // on each frame
51 GameConfig playconfig;
54 transient int otherSeed, roomSeed;
57 override void Destroy () {
59 keypresses.length = 0;
64 final void resetReplay () {
69 #ifndef BIGGER_REPLAY_DATA
70 final void addKey (int kbidx, bool down) {
71 if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
72 keypresses[$] = kbidx|(down ? 0x80 : 0);
76 final void addEndOfFrame () {
87 final int getKey (out int kbidx, out bool down) {
88 if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
89 if (keypos >= keypresses.length) return END_OF_RECORD;
90 ubyte b = keypresses[keypos++];
91 if (b == 0xff) return END_OF_FRAME;
99 // ////////////////////////////////////////////////////////////////////////// //
100 class TempOptionsKeys : Object;
102 int[16*GameConfig::MaxActionBinds] keybinds;
106 // ////////////////////////////////////////////////////////////////////////// //
109 transient string dbgSessionStateFileName = "debug_game_session_state";
110 transient string dbgSessionMovementFileName = "debug_game_session_movement";
111 const float dbgSessionSaveIntervalInSeconds = 30;
113 GLTexture texTigerEye;
117 SpriteStore sprStore;
118 BackTileStore bgtileStore;
121 int mouseX = int.min, mouseY = int.min;
122 int mouseLevelX = int.min, mouseLevelY = int.min;
123 bool renderMouseTile;
124 bool renderMouseRect;
136 StartMode startMode = StartMode.Intro;
140 bool replayFastForward = false;
141 int replayFastForwardSpeed = 2;
142 bool saveGameSession = false;
143 bool replayGameSession = false;
149 Replay doGameSavingPlaying = Replay.None;
150 float saveMovementLastTime = 0;
151 DebugSessionMovement debugMovement;
152 GameStats origStats; // for replaying
153 GameConfig origConfig; // for replaying
154 GameGlobal::SavedSeeds origSeeds;
160 transient bool allowRender = true;
164 transient int maskSX, maskSY;
165 transient SpriteImage smask;
166 transient int maskFrame;
170 // ////////////////////////////////////////////////////////////////////////// //
171 final void saveKeyboardBindings () {
172 auto tok = SpawnObject(TempOptionsKeys);
173 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
174 appSaveOptions(tok, "keybindings");
179 final void loadKeyboardBindings () {
180 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
182 if (tok.kbversion != TempOptionsKeys.default.kbversion) {
183 global.config.resetKeybindings();
185 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
192 // ////////////////////////////////////////////////////////////////////////// //
193 void saveGameOptions () {
194 appSaveOptions(global.config, "config");
198 void loadGameOptions () {
199 auto cfg = appLoadOptions(GameConfig, "config");
201 auto oldHero = config.heroType;
202 auto tok = SpawnObject(TempOptionsKeys);
203 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
204 delete global.config;
207 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
209 writeln("config loaded");
210 global.restartMusic();
212 //config.heroType = GameConfig::Hero.Spelunker;
213 config.heroType = oldHero;
216 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
220 // ////////////////////////////////////////////////////////////////////////// //
221 void saveGameStats () {
222 if (level.stats) appSaveOptions(level.stats, "stats");
226 void loadGameStats () {
227 auto stats = appLoadOptions(GameStats, "stats");
232 if (!level.stats) level.stats = SpawnObject(GameStats);
233 level.stats.global = global;
237 // ////////////////////////////////////////////////////////////////////////// //
238 struct UIPaneSaveInfo {
240 UIPane::SaveInfo nfo;
243 transient UIPane optionsPane; // either options, or binding editor
245 transient GameLevel::IVec2D optionsPaneOfs;
246 transient void delegate () saveOptionsDG;
248 transient array!UIPaneSaveInfo optionsPaneState;
251 final void saveCurrentPane () {
252 if (!optionsPane || !optionsPane.id) return;
255 if (optionsPane.id == 'CheatFlags') {
256 if (instantGhost && level.ghostTimeLeft > 0) {
257 level.ghostTimeLeft = 1;
261 foreach (ref auto psv; optionsPaneState) {
262 if (psv.id == optionsPane.id) {
263 optionsPane.saveState(psv.nfo);
268 optionsPaneState.length += 1;
269 optionsPaneState[$-1].id = optionsPane.id;
270 optionsPane.saveState(optionsPaneState[$-1].nfo);
274 final void restoreCurrentPane () {
275 if (optionsPane) optionsPane.setupHotkeys(); // why not?
276 if (!optionsPane || !optionsPane.id) return;
277 foreach (ref auto psv; optionsPaneState) {
278 if (psv.id == optionsPane.id) {
279 optionsPane.restoreState(psv.nfo);
286 // ////////////////////////////////////////////////////////////////////////// //
287 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
288 if (!it.tagClass) return;
289 if (class!MapObject(it.tagClass)) {
290 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
291 it.owner.closeMe = true;
296 // ////////////////////////////////////////////////////////////////////////// //
297 transient array!(class!MapObject) cheatItemsList;
300 final void fillCheatItemsList () {
301 cheatItemsList.length = 0;
302 cheatItemsList[$] = ItemProjectileArrow;
303 cheatItemsList[$] = ItemWeaponShotgun;
304 cheatItemsList[$] = ItemWeaponAshShotgun;
305 cheatItemsList[$] = ItemWeaponPistol;
306 cheatItemsList[$] = ItemWeaponMattock;
307 cheatItemsList[$] = ItemWeaponMachete;
308 cheatItemsList[$] = ItemWeaponWebCannon;
309 cheatItemsList[$] = ItemWeaponSceptre;
310 cheatItemsList[$] = ItemWeaponBow;
311 cheatItemsList[$] = ItemBones;
312 cheatItemsList[$] = ItemFakeBones;
313 cheatItemsList[$] = ItemFishBone;
314 cheatItemsList[$] = ItemRock;
315 cheatItemsList[$] = ItemJar;
316 cheatItemsList[$] = ItemSkull;
317 cheatItemsList[$] = ItemGoldenKey;
318 cheatItemsList[$] = ItemGoldIdol;
319 cheatItemsList[$] = ItemCrystalSkull;
320 cheatItemsList[$] = ItemShellSingle;
321 cheatItemsList[$] = ItemChest;
322 cheatItemsList[$] = ItemCrate;
323 cheatItemsList[$] = ItemLockedChest;
324 cheatItemsList[$] = ItemDice;
325 cheatItemsList[$] = ItemBasketBall;
329 final UIPane createCheatItemsPane () {
330 if (!level.player) return none;
332 UIPane pane = SpawnObject(UIPane);
334 pane.sprStore = sprStore;
336 pane.width = 320*3-64;
337 pane.height = 240*3-64;
339 foreach (auto ipk; cheatItemsList) {
340 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
344 //optionsPaneOfs.x = 100;
345 //optionsPaneOfs.y = 50;
351 // ////////////////////////////////////////////////////////////////////////// //
352 transient array!(class!MapObject) cheatEnemiesList;
355 final void fillCheatEnemiesList () {
356 cheatEnemiesList.length = 0;
357 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
358 cheatEnemiesList[$] = EnemyBat;
359 cheatEnemiesList[$] = EnemySpiderHang;
360 cheatEnemiesList[$] = EnemySpider;
361 cheatEnemiesList[$] = EnemySnake;
362 cheatEnemiesList[$] = EnemyCaveman;
363 cheatEnemiesList[$] = EnemySkeleton;
364 cheatEnemiesList[$] = MonsterShopkeeper;
365 cheatEnemiesList[$] = EnemyZombie;
366 cheatEnemiesList[$] = EnemyVampire;
367 cheatEnemiesList[$] = EnemyFrog;
368 cheatEnemiesList[$] = EnemyGreenFrog;
369 cheatEnemiesList[$] = EnemyFireFrog;
370 cheatEnemiesList[$] = EnemyMantrap;
371 cheatEnemiesList[$] = EnemyScarab;
372 cheatEnemiesList[$] = EnemyFloater;
373 cheatEnemiesList[$] = EnemyBlob;
374 cheatEnemiesList[$] = EnemyMonkey;
375 cheatEnemiesList[$] = EnemyGoldMonkey;
376 cheatEnemiesList[$] = EnemyAlien;
377 cheatEnemiesList[$] = EnemyYeti;
378 cheatEnemiesList[$] = EnemyHawkman;
379 cheatEnemiesList[$] = EnemyUFO;
380 cheatEnemiesList[$] = EnemyYetiKing;
384 final UIPane createCheatEnemiesPane () {
385 if (!level.player) return none;
387 UIPane pane = SpawnObject(UIPane);
389 pane.sprStore = sprStore;
391 pane.width = 320*3-64;
392 pane.height = 240*3-64;
394 foreach (auto ipk; cheatEnemiesList) {
395 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
399 //optionsPaneOfs.x = 100;
400 //optionsPaneOfs.y = 50;
406 // ////////////////////////////////////////////////////////////////////////// //
407 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
410 final void fillCheatPickupList () {
411 cheatPickupList.length = 0;
412 cheatPickupList[$] = ItemPickupBombBag;
413 cheatPickupList[$] = ItemPickupBombBox;
414 cheatPickupList[$] = ItemPickupPaste;
415 cheatPickupList[$] = ItemPickupRopePile;
416 cheatPickupList[$] = ItemPickupShellBox;
417 cheatPickupList[$] = ItemPickupAnkh;
418 cheatPickupList[$] = ItemPickupCape;
419 cheatPickupList[$] = ItemPickupJetpack;
420 cheatPickupList[$] = ItemPickupUdjatEye;
421 cheatPickupList[$] = ItemPickupCrown;
422 cheatPickupList[$] = ItemPickupKapala;
423 cheatPickupList[$] = ItemPickupParachute;
424 cheatPickupList[$] = ItemPickupCompass;
425 cheatPickupList[$] = ItemPickupSpectacles;
426 cheatPickupList[$] = ItemPickupGloves;
427 cheatPickupList[$] = ItemPickupMitt;
428 cheatPickupList[$] = ItemPickupJordans;
429 cheatPickupList[$] = ItemPickupSpringShoes;
430 cheatPickupList[$] = ItemPickupSpikeShoes;
431 cheatPickupList[$] = ItemPickupTeleporter;
435 final UIPane createCheatPickupsPane () {
436 if (!level.player) return none;
438 UIPane pane = SpawnObject(UIPane);
440 pane.sprStore = sprStore;
442 pane.width = 320*3-64;
443 pane.height = 240*3-64;
445 foreach (auto ipk; cheatPickupList) {
446 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
450 //optionsPaneOfs.x = 100;
451 //optionsPaneOfs.y = 50;
457 // ////////////////////////////////////////////////////////////////////////// //
458 transient int instantGhost;
460 final UIPane createCheatFlagsPane () {
461 UIPane pane = SpawnObject(UIPane);
462 pane.id = 'CheatFlags';
463 pane.sprStore = sprStore;
465 pane.width = 320*3-64;
466 pane.height = 240*3-64;
470 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
471 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
472 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
473 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
474 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
475 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
476 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
477 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
478 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
479 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
480 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
481 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
482 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
483 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
484 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
485 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
486 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
487 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
489 optionsPaneOfs.x = 100;
490 optionsPaneOfs.y = 50;
496 final UIPane createOptionsPane () {
497 UIPane pane = SpawnObject(UIPane);
499 pane.sprStore = sprStore;
501 pane.width = 320*3-64;
502 pane.height = 240*3-64;
506 //!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.");
509 UILabel.Create(pane, "VISUAL OPTIONS");
510 UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
511 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
512 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).");
513 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
514 auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
515 startfs.onValueChanged = delegate void (int newval) {
516 Video.showMouseCursor();
521 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).");
522 fsmode.names[$] = "REAL";
523 fsmode.names[$] = "SCALED";
524 fsmode.onValueChanged = delegate void (int newval) {
526 Video.showMouseCursor();
533 UILabel.Create(pane, "");
534 UILabel.Create(pane, "HUD OPTIONS");
535 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
536 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.");
537 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
540 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
544 UILabel.Create(pane, "");
545 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
546 //!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.");
547 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
548 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
549 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.");
550 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.");
551 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
552 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.");
553 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
556 UILabel.Create(pane, "");
557 UILabel.Create(pane, "GAMEPLAY OPTIONS");
558 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.");
559 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
560 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
561 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!");
562 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.");
563 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.");
564 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
565 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.");
566 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.");
567 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.");
568 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.");
569 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?");
570 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.");
571 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.");
572 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.");
573 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
574 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
575 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
576 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
577 rstl.names[$] = "RANDOM";
578 rstl.names[$] = "NORMAL";
579 rstl.names[$] = "BIZARRE";
582 UILabel.Create(pane, "");
583 UILabel.Create(pane, "WHIP OPTIONS");
584 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
585 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
586 whiptype.names[$] = "NORMAL";
587 whiptype.names[$] = "LONG";
588 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.");
591 UILabel.Create(pane, "");
592 UILabel.Create(pane, "PLAYER OPTIONS");
593 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
594 herotype.names[$] = "SPELUNKY GUY";
595 herotype.names[$] = "DAMSEL";
596 herotype.names[$] = "TUNNEL MAN";
599 UILabel.Create(pane, "");
600 UILabel.Create(pane, "CHEAT OPTIONS");
601 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.");
602 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
603 plrlit.names[$] = "NEVER";
604 plrlit.names[$] = "FORCED DARKNESS";
605 plrlit.names[$] = "ALWAYS";
606 UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
607 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'.");
608 rdark.names[$] = "NEVER";
609 rdark.names[$] = "DEFAULT";
610 rdark.names[$] = "ALWAYS";
611 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.");
613 rghost.getNameCB = delegate string (int val) {
614 if (val < 0) return "INSTANT";
615 if (val == 0) return "NEVER";
616 if (val < 120) return va("%d SEC", val);
617 if (val%60 == 0) return va("%d MIN", val/60);
618 if (val%60 == 30) return va("%d.5 MIN", val/60);
619 return va("%d MIN, %d SEC", val/60, val%60);
621 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.");
623 UILabel.Create(pane, "");
624 UILabel.Create(pane, "CHEAT START OPTIONS");
625 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.");
626 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
627 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
628 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
629 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
632 UILabel.Create(pane, "");
633 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
634 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
635 mm.names[$] = "SILENCE";
636 mm.names[$] = "RESTART";
637 mm.names[$] = "DON'T TOUCH";
639 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
640 //mm.names[$] = "SILENCE";
641 mm.names[$] = "RESTART";
642 mm.names[$] = "DON'T TOUCH";
645 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
647 swstereo.onValueChanged = delegate void (int newval) {
648 SoundSystem.SwapStereo = newval;
652 UILabel.Create(pane, "");
653 UILabel.Create(pane, "SOUND CONTROL CENTER");
654 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
655 rmusonoff.onValueChanged = delegate void (int newval) {
656 global.restartMusic();
659 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
661 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
662 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
664 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
665 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
668 saveOptionsDG = delegate void () {
669 writeln("saving options");
672 optionsPaneOfs.x = 42;
673 optionsPaneOfs.y = 0;
679 final void createBindingsControl (UIPane pane, int keyidx) {
682 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
683 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
684 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
685 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
686 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
687 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
688 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
689 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
690 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
691 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
692 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
695 int arridx = GameConfig.getKeyIndex(keyidx);
696 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
700 final UIPane createBindingsPane () {
701 UIPane pane = SpawnObject(UIPane);
702 pane.id = 'KeyBindings';
703 pane.sprStore = sprStore;
705 pane.width = 320*3-64;
706 pane.height = 240*3-64;
708 createBindingsControl(pane, GameConfig::Key.Left);
709 createBindingsControl(pane, GameConfig::Key.Right);
710 createBindingsControl(pane, GameConfig::Key.Up);
711 createBindingsControl(pane, GameConfig::Key.Down);
712 createBindingsControl(pane, GameConfig::Key.Jump);
713 createBindingsControl(pane, GameConfig::Key.Run);
714 createBindingsControl(pane, GameConfig::Key.Attack);
715 createBindingsControl(pane, GameConfig::Key.Switch);
716 createBindingsControl(pane, GameConfig::Key.Pay);
717 createBindingsControl(pane, GameConfig::Key.Bomb);
718 createBindingsControl(pane, GameConfig::Key.Rope);
720 saveOptionsDG = delegate void () {
721 writeln("saving keys");
722 saveKeyboardBindings();
724 optionsPaneOfs.x = 120;
725 optionsPaneOfs.y = 140;
731 // ////////////////////////////////////////////////////////////////////////// //
732 void clearGameMovement () {
733 debugMovement = SpawnObject(DebugSessionMovement);
734 debugMovement.playconfig = SpawnObject(GameConfig);
735 debugMovement.playconfig.copyGameplayConfigFrom(config);
736 debugMovement.resetReplay();
740 void saveGameMovement (string fname, optional bool packit) {
741 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
742 saveMovementLastTime = GetTickCount();
746 void loadGameMovement (string fname) {
747 delete debugMovement;
748 debugMovement = appLoadOptions(DebugSessionMovement, fname);
749 debugMovement.resetReplay();
752 origStats = level.stats;
753 origStats.global = none;
754 level.stats = SpawnObject(GameStats);
755 level.stats.global = global;
758 config = debugMovement.playconfig;
759 global.config = config;
760 global.saveSeeds(origSeeds);
765 void stopReplaying () {
767 global.restoreSeeds(origSeeds);
769 delete debugMovement;
770 saveGameSession = false;
771 replayGameSession = false;
772 doGameSavingPlaying = Replay.None;
775 origStats.global = global;
776 level.stats = origStats;
782 global.config = origConfig;
788 // ////////////////////////////////////////////////////////////////////////// //
789 final bool saveGame (string gmname) {
790 return appSaveOptions(level, gmname);
794 final bool loadGame (string gmname) {
795 auto olddel = ImmediateDelete;
796 ImmediateDelete = false;
798 auto stats = level.stats;
801 auto lvl = appLoadOptions(GameLevel, gmname);
803 //lvl.global.config = config;
808 global = level.global;
809 global.config = config;
811 level.sprStore = sprStore;
812 level.bgtileStore = bgtileStore;
815 level.onBeforeFrame = &beforeNewFrame;
816 level.onAfterFrame = &afterNewFrame;
817 level.onInterFrame = &interFrame;
818 level.onLevelExitedCB = &levelExited;
819 level.onCameraTeleported = &cameraTeleportedCB;
821 //level.viewWidth = Video.screenWidth;
822 //level.viewHeight = Video.screenHeight;
823 level.viewWidth = 320*3;
824 level.viewHeight = 240*3;
827 level.centerViewAtPlayer();
828 teleportCameraAt(level.viewStart);
830 recalcCameraCoords(0);
835 level.stats.global = level.global;
837 ImmediateDelete = olddel;
838 CollectGarbage(true); // destroy delayed objects too
843 // ////////////////////////////////////////////////////////////////////////// //
844 float lastThinkerTime;
845 int replaySkipFrame = 0;
848 final void onTimePasses () {
849 float curTime = GetTickCount();
850 if (lastThinkerTime > 0) {
851 if (curTime < lastThinkerTime) {
852 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
853 lastThinkerTime = curTime;
856 if (replayFastForward && replaySkipFrame) {
858 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
861 level.processThinkers(curTime-lastThinkerTime);
863 lastThinkerTime = curTime;
867 final void resetFramesAndForceOne () {
868 float curTime = GetTickCount();
869 lastThinkerTime = curTime;
871 auto wasPaused = level.gamePaused;
872 level.gamePaused = false;
873 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
874 level.processThinkers(GameLevel::FrameTime);
875 level.gamePaused = wasPaused;
876 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
880 // ////////////////////////////////////////////////////////////////////////// //
881 private float currFrameDelta; // so level renderer can properly interpolate the player
882 private GameLevel::IVec2D camPrev, camCurr;
883 private GameLevel::IVec2D camShake;
884 private GameLevel::IVec2D viewCameraPos;
887 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
892 viewCameraPos.x = pos.x;
893 viewCameraPos.y = pos.y;
899 // call `recalcCameraCoords()` to get real camera coords after this
900 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
901 // check if camera is moved too far, and teleport it
903 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
904 abs(camCurr.y-pos.y)/global.scale >= 16*4))
906 teleportCameraAt(pos);
908 camPrev.x = camCurr.x;
909 camPrev.y = camCurr.y;
913 camShake.x = level.shakeDir.x*global.scale;
914 camShake.y = level.shakeDir.y*global.scale;
918 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
919 currFrameDelta = frameDelta;
920 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
921 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
923 viewCameraPos.x += camShake.x;
924 viewCameraPos.y += camShake.y;
928 GameLevel::SavedKeyState savedKeyState;
930 final void pauseGame () {
931 if (!level.gamePaused) {
932 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
933 level.gamePaused = true;
934 global.pauseAllSounds();
939 final void unpauseGame () {
940 if (level.gamePaused) {
941 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
942 level.gamePaused = false;
943 level.gameShowHelp = false;
944 level.gameHelpScreen = 0;
945 //lastThinkerTime = 0;
946 global.resumeAllSounds();
948 pauseRequested = false;
949 helpRequested = false;
954 final void beforeNewFrame (bool frameSkip) {
957 level.disablePlayerThink = true;
960 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
961 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
962 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
964 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
965 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
966 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
967 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
969 level.disablePlayerThink = false;
975 if (!level.gamePaused) {
976 // save seeds for afterframe processing
978 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
979 debugMovement.otherSeed = global.globalOtherSeed;
980 debugMovement.roomSeed = global.globalRoomSeed;
984 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
986 #ifdef BIGGER_REPLAY_DATA
987 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
988 debugMovement.keypresses.length += 1;
989 level.keysSaveState(debugMovement.keypresses[$-1]);
990 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
991 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
995 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
996 #ifdef BIGGER_REPLAY_DATA
997 if (debugMovement.keypos < debugMovement.keypresses.length) {
998 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
999 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1000 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1001 ++debugMovement.keypos;
1007 auto code = debugMovement.getKey(out kbidx, out down);
1008 if (code == DebugSessionMovement::END_OF_RECORD) {
1009 // do this in main loop, so we can view totals
1013 if (code == DebugSessionMovement::END_OF_FRAME) {
1016 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1017 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1025 final void afterNewFrame (bool frameSkip) {
1026 if (!replayFastForward) replaySkipFrame = 0;
1028 if (level.gamePaused) return;
1030 if (!level.gamePaused) {
1031 if (doGameSavingPlaying != Replay.None) {
1032 if (doGameSavingPlaying == Replay.Saving) {
1033 replayFastForward = false; // just in case
1034 #ifndef BIGGER_REPLAY_DATA
1035 debugMovement.addEndOfFrame();
1037 auto stt = GetTickCount();
1038 if (stt-saveMovementLastTime >= dbgSessionSaveIntervalInSeconds) saveGameMovement(dbgSessionMovementFileName);
1039 } else if (doGameSavingPlaying == Replay.Replaying) {
1040 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1041 replaySkipFrame = 1;
1047 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1048 //SoundSystem.UpdateSounds();
1050 //if (!freeRide) level.fixCamera();
1051 setNewCameraPos(level.viewStart);
1053 prevCameraX = currCameraX;
1054 prevCameraY = currCameraY;
1055 currCameraX = level.cameraX;
1056 currCameraY = level.cameraY;
1057 // disable camera interpolation if the screen is shaking
1058 if (level.shakeX|level.shakeY) {
1059 prevCameraX = currCameraX;
1060 prevCameraY = currCameraY;
1063 // disable camera interpolation if it moves too far away
1064 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1065 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1067 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1069 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1070 pauseRequested = false;
1072 if (helpRequested) {
1073 helpRequested = false;
1074 level.gameShowHelp = true;
1075 level.gameHelpScreen = 0;
1078 if (!showHelp) showHelp = true;
1080 writeln("active objects in level: ", level.activeItemsCount);
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);
1432 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1435 if (level.gamePaused && showHelp != 2) {
1436 if (mouseLevelX != int.min) {
1437 int scale = level.global.scale;
1438 if (renderMouseRect) {
1439 Video.color = 0xcf_ff_ff_00;
1440 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1442 if (renderMouseTile) {
1443 Video.color = 0xaf_ff_00_00;
1444 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1449 switch (doGameSavingPlaying) {
1451 Video.color = 0x7f_00_ff_00;
1452 sprStore.loadFont('sFont');
1453 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1455 case Replay.Replaying:
1456 if (level.player && !level.player.dead) {
1457 Video.color = 0x7f_ff_00_00;
1458 sprStore.loadFont('sFont');
1459 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1460 int th = sprStore.getFontHeight(2);
1461 if (replayFastForward) {
1462 sprStore.loadFont('sFontSmall');
1463 string sstr = va("x%d", replayFastForwardSpeed+1);
1464 sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1469 if (saveGameSession) {
1470 Video.color = 0x7f_ff_7f_00;
1471 sprStore.loadFont('sFont');
1472 sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1478 if (level.player && level.player.dead && !showHelp) {
1480 Video.color = 0x8f_00_00_00;
1481 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1486 if (true /*level.inWinCutscene == 0*/) {
1487 Video.color = 0xff_ff_ff;
1488 sprStore.loadFont('sFontSmall');
1489 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1491 "PRESS $PAY TO RESTART GAME\n"~
1493 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1495 "TOTAL PLAYING TIME: |%s|"~
1497 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1498 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1499 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1501 GameLevel.time2str(level.stats.playingTime)
1503 kmsg = global.expandString(kmsg);
1504 sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1511 Video.color = 0xff_7f_00;
1512 sprStore.loadFont('sFontSmall');
1513 sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1514 auto spf = smask.frames[maskFrame];
1515 sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1517 spf.bx, spf.by, spf.bw, spf.bh,
1518 (spf.maskEmpty ? "TAN" : "ONA"),
1519 (spf.precise ? "TAN" : "ONA")),
1522 //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1523 //writeln("pos=(", maskSX, ",", maskSY, ")");
1524 int scale = global.config.scale;
1525 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1526 int mapX = xofs/scale+maskSX;
1527 int mapY = yofs/scale+maskSY;
1530 writeln("==== tiles ====");
1532 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1533 if (t.spectral || !t.isInstanceAlive) return false;
1534 Video.color = 0x7f_ff_00_00;
1535 Video.fillRect(t.x0*global.config.scale-viewCameraPos.x, t.y0*global.config.scale-viewCameraPos.y, t.width*global.config.scale, t.height*global.config.scale);
1536 auto tsf = t.getSpriteFrame();
1538 auto spf = smask.frames[maskFrame];
1539 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1540 int mapX = xofs/global.config.scale+maskSX;
1541 int mapY = yofs/global.config.scale+maskSY;
1544 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1545 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1546 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1550 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1551 Video.color = 0x7f_ff_00_00;
1552 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);
1556 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1558 Video.color = 0xaf_ff_ff_ff;
1559 spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1560 Video.color = 0xff_ff_00;
1561 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1565 int fx0, fy0, fx1, fy1;
1566 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1567 Video.color = 0x7f_00_00_ff;
1568 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1574 Video.color = 0x8f_00_00_00;
1575 Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1577 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1582 Video.color = 0xff_ff_00;
1583 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1584 if (showHelp == 1) {
1585 int msx, msy, ww, wh;
1586 Video.getMousePos(out msx, out msy);
1587 Video.getRealWindowSize(out ww, out wh);
1588 if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1589 sprStore.loadFont('sFontSmall');
1590 Video.color = 0xff_ff_00;
1591 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1592 "F1: show this help\n"~
1594 "K : redefine keys\n"~
1595 "I : toggle interpolaion\n"~
1596 "N : create some blood\n"~
1597 "R : generate a new level\n"~
1598 "F : toggle \"Frozen Area\"\n"~
1599 "X : resurrect player\n"~
1600 "Q : teleport to exit\n"~
1601 "D : teleport to damel\n"~
1603 "C : cheat flags menu\n"~
1604 "P : cheat pickup menu\n"~
1605 "E : cheat enemy menu\n"~
1606 "Enter: cheat items menu\n"~
1608 "TAB: toggle 'freeroam' mode\n"~
1613 if (level) level.renderPauseOverlay();
1617 //SoundSystem.UpdateSounds();
1619 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1622 Video.setScissor(scsave);
1623 Video.glPopMatrix();
1628 Video.color = 0xaf_ff_ff_ff;
1629 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1634 // ////////////////////////////////////////////////////////////////////////// //
1635 transient bool gameJustOver;
1636 transient bool waitingForPayRestart;
1639 final void calcMouseMapCoords () {
1640 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1641 mouseLevelX = int.min;
1642 mouseLevelY = int.min;
1645 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1646 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1647 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1651 final void onEvent (ref event_t evt) {
1652 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1654 if (evt.type == ev_winfocus) {
1655 if (level && !evt.focused) {
1660 //writeln("FOCUS!");
1661 Video.getMousePos(out mouseX, out mouseY);
1666 if (evt.type == ev_mouse) {
1669 calcMouseMapCoords();
1672 if (evt.type == ev_keydown && evt.keycode == K_F12) {
1673 if (level) toggleFullscreen();
1677 if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1678 writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1679 writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1682 if (evt.type == ev_keydown) {
1683 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1684 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1685 renderMouseTile = evt.bCtrl;
1686 renderMouseRect = evt.bAlt;
1689 if (evt.type == ev_keyup) {
1690 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1691 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1692 renderMouseTile = evt.bCtrl;
1693 renderMouseRect = evt.bAlt;
1696 if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1698 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1699 int newScale = evt.keycode-48;
1700 if (global.config.scale != newScale) {
1701 global.config.scale = newScale;
1704 cameraTeleportedCB();
1711 if (evt.type == ev_mouse) {
1712 maskSX = evt.x/global.config.scale;
1713 maskSY = evt.y/global.config.scale;
1716 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1717 maskFrame = max(0, maskFrame-1);
1720 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1721 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1730 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1732 if (saveOptionsDG) saveOptionsDG();
1733 saveOptionsDG = none;
1735 //SoundSystem.UpdateSounds(); // just in case
1736 if (global.hasSpectacles) level.pickedSpectacles();
1739 optionsPane.onEvent(evt);
1743 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1744 if (evt.type == ev_keydown) {
1745 if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1746 switch (evt.keycode) {
1747 case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1748 case K_F2: if (showHelp != 2) unpauseGame(); return;
1749 case K_F10: Video.requestQuit(); return;
1750 case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1754 allowRender = !allowRender;
1760 case K_UPARROW: case K_PAD8:
1761 if (drawStats) statsMoveUp();
1764 case K_DOWNARROW: case K_PAD2:
1765 if (drawStats) statsMoveDown();
1768 case K_LEFTARROW: case K_PAD4:
1769 if (level && showHelp == 2 && level.gameShowHelp) {
1770 if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1774 case K_RIGHTARROW: case K_PAD6:
1775 if (level && showHelp == 2 && level.gameShowHelp) {
1776 level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1790 resetFramesAndForceOne();
1796 if (/*evt.bCtrl &&*/ showHelp != 2) {
1806 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1807 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1808 case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1809 case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1810 case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1811 case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1812 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1813 //case K_j: global.hasJordans = !global.hasJordans; return;
1815 if (/*evt.bCtrl &&*/ showHelp != 2) {
1816 level.resurrectPlayer();
1821 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1822 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1823 if (evt.bAlt && level.player && level.player.dead) {
1824 saveGameSession = false;
1825 replayGameSession = true;
1829 if (/*evt.bCtrl &&*/ showHelp != 2) {
1830 if (evt.bShift) global.idol = false;
1831 level.generateLevel();
1832 level.centerViewAtPlayer();
1833 teleportCameraAt(level.viewStart);
1834 resetFramesAndForceOne();
1838 global.toggleMusic();
1841 if (/*evt.bCtrl &&*/ showHelp != 2) {
1842 foreach (MapTile t; level.allExits) {
1843 if (!level.isSolidAtPoint(t.ix+8, t.iy+8)) {
1844 level.teleportPlayerTo(t.ix+8, t.iy+8);
1852 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1853 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1855 level.teleportPlayerTo(damsel.ix, damsel.iy);
1861 if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1865 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1868 obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1871 level.teleportPlayerTo(obj.ix, obj.iy-4);
1877 if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1878 if (level && mouseLevelX != int.min) {
1879 int scale = level.global.scale;
1880 int mapX = mouseLevelX;
1881 int mapY = mouseLevelY;
1882 level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1888 if (evt.bCtrl && showHelp != 2) {
1889 if (level && mouseLevelX != int.min) {
1890 int scale = level.global.scale;
1891 int mapX = mouseLevelX;
1892 int mapY = mouseLevelY;
1893 level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1899 if (evt.bCtrl && showHelp != 2) {
1900 if (level && mouseLevelX != int.min) {
1901 int scale = level.global.scale;
1902 int mapX = mouseLevelX;
1903 int mapY = mouseLevelY;
1904 level.RemoveMapTileFromGrid(mapX/16, mapY/16, "arrow trap");
1905 level.MakeMapTile(mapX/16, mapY/16, (level.player.dir == MapObject::Dir.Left ? 'oArrowTrapLeft' : 'oArrowTrapRight'));
1911 if (evt.bCtrl && showHelp != 2) {
1912 if (level && mouseLevelX != int.min) {
1913 int scale = level.global.scale;
1914 int mapX = mouseLevelX;
1915 int mapY = mouseLevelY;
1916 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1920 if (evt.bAlt && showHelp != 2) {
1921 if (level && mouseLevelX != int.min) {
1922 int scale = level.global.scale;
1923 int mapX = mouseLevelX;
1924 int mapY = mouseLevelY;
1925 level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1931 if (level && mouseLevelX != int.min) {
1932 int scale = level.global.scale;
1933 int mapX = mouseLevelX;
1934 int mapY = mouseLevelY;
1937 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1938 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1939 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1943 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1944 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1950 if (evt.bShift && showHelp != 2 && level && mouseLevelX != int.min) {
1951 auto obj = level.MakeMapTile(mouseLevelX/16, mouseLevelY/16, 'oBoulder');
1955 case K_DELETE: // suicide
1956 if (doGameSavingPlaying == Replay.None) {
1957 if (level.player && !level.player.dead && evt.bCtrl) {
1958 global.hasAnkh = false;
1959 level.global.plife = 1;
1960 level.player.invincible = 0;
1961 auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1962 if (xplo) xplo.suicide = true;
1969 if (level.player && !level.player.dead && evt.bAlt) {
1970 if (doGameSavingPlaying != Replay.None) {
1971 if (doGameSavingPlaying == Replay.Replaying) {
1973 } else if (doGameSavingPlaying == Replay.Saving) {
1974 saveGameMovement(dbgSessionMovementFileName, packit:true);
1976 doGameSavingPlaying = Replay.None;
1978 saveGameSession = false;
1979 replayGameSession = false;
1986 if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1987 level.stats.setMoneyCheat();
1988 level.stats.addMoney(10000);
1994 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1995 if (level.player && level.player.dead) {
1996 //Video.requestQuit();
1998 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
2000 #ifdef QUIT_DOUBLE_ESC
2001 if (++escCount == 2) Video.requestQuit();
2004 pauseRequested = true;
2010 if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
2011 if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2012 if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
2015 //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
2018 if (!level.player || !level.player.dead) {
2019 gameJustOver = false;
2020 } else if (level.player && level.player.dead) {
2021 if (!gameJustOver) {
2023 gameJustOver = true;
2024 waitingForPayRestart = true;
2025 level.clearKeysPressRelease();
2026 if (doGameSavingPlaying == Replay.None) {
2027 stopReplaying(); // just in case
2031 replayFastForward = false;
2032 if (doGameSavingPlaying == Replay.Saving) {
2033 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2034 doGameSavingPlaying = Replay.None;
2035 //clearGameMovement();
2036 saveGameSession = false;
2037 replayGameSession = false;
2040 if (evt.type == ev_keydown || evt.type == ev_keyup) {
2041 bool down = (evt.type == ev_keydown);
2042 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2043 if (down && evt.keycode == K_f) {
2045 if (replayFastForwardSpeed != 4) {
2046 replayFastForwardSpeed = 4;
2047 replayFastForward = true;
2049 replayFastForward = !replayFastForward;
2052 replayFastForwardSpeed = 2;
2053 replayFastForward = !replayFastForward;
2057 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2058 foreach (int kbidx, int kval; global.config.keybinds) {
2059 if (kval && kval == evt.keycode) {
2060 #ifndef BIGGER_REPLAY_DATA
2061 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2063 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2067 if (level.player && level.player.dead) {
2068 if (down && evt.keycode == K_r && evt.bAlt) {
2069 saveGameSession = false;
2070 replayGameSession = true;
2073 if (down && evt.keycode == K_s && evt.bAlt) {
2074 bool wasSaveReq = saveGameSession;
2075 stopReplaying(); // just in case
2076 saveGameSession = !wasSaveReq;
2077 replayGameSession = false;
2080 if (replayGameSession) {
2081 stopReplaying(); // just in case
2082 saveGameSession = false;
2083 replayGameSession = false;
2084 loadGameMovement(dbgSessionMovementFileName);
2085 loadGame(dbgSessionStateFileName);
2086 doGameSavingPlaying = Replay.Replaying;
2089 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2090 if (down && (evt.keycode == K_UPARROW || evt.keycode == K_PAD8) && !evt.bAlt && drawStats) statsMoveUp();
2091 if (down && (evt.keycode == K_DOWNARROW || evt.keycode == K_PAD2) && !evt.bAlt && drawStats) statsMoveDown();
2092 if (waitingForPayRestart) {
2093 level.isKeyReleased(GameConfig::Key.Pay);
2094 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2096 level.isKeyPressed(GameConfig::Key.Pay);
2097 if (level.isKeyReleased(GameConfig::Key.Pay)) {
2098 auto doSave = saveGameSession;
2099 stopReplaying(); // just in case
2100 level.clearKeysPressRelease();
2101 level.restartGame();
2102 level.generateNormalLevel();
2104 saveGameSession = false;
2105 replayGameSession = false;
2106 writeln("DBG: saving game session...");
2107 clearGameMovement();
2108 doGameSavingPlaying = Replay.Saving;
2109 saveGame(dbgSessionStateFileName);
2110 //saveGameMovement(dbgSessionMovementFileName);
2121 void levelExited () {
2127 void initializeVideo () {
2128 Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2129 if (Video.realStencilBits < 8) {
2130 Video.closeScreen();
2131 FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2133 if (!Video.framebufferHasAlpha) {
2134 Video.closeScreen();
2135 FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!");
2137 if (fullscreen) Video.hideMouseCursor();
2141 void toggleFullscreen () {
2142 Video.showMouseCursor();
2143 Video.closeScreen();
2144 fullscreen = !fullscreen;
2149 final void runGameLoop () {
2150 Video.frameTime = 0; // unlimited FPS
2151 lastThinkerTime = 0;
2153 sprStore = SpawnObject(SpriteStore);
2154 sprStore.bDumpLoaded = false;
2156 bgtileStore = SpawnObject(BackTileStore);
2157 bgtileStore.bDumpLoaded = false;
2159 level = SpawnObject(GameLevel);
2160 level.setup(global, sprStore, bgtileStore);
2162 level.BuildYear = BuildYear;
2163 level.BuildMonth = BuildMonth;
2164 level.BuildDay = BuildDay;
2165 level.BuildHour = BuildHour;
2166 level.BuildMin = BuildMin;
2168 level.global = global;
2169 level.sprStore = sprStore;
2170 level.bgtileStore = bgtileStore;
2173 //level.stats.introViewed = 0;
2175 if (level.stats.introViewed == 0) {
2176 startMode = StartMode.Intro;
2177 writeln("FORCED INTRO");
2179 //writeln("INTRO VIWED: ", level.stats.introViewed);
2180 if (level.global.config.skipIntro) startMode = StartMode.Title;
2183 level.onBeforeFrame = &beforeNewFrame;
2184 level.onAfterFrame = &afterNewFrame;
2185 level.onInterFrame = &interFrame;
2186 level.onLevelExitedCB = &levelExited;
2187 level.onCameraTeleported = &cameraTeleportedCB;
2190 maskSX = -0x0ff_fff;
2192 smask = sprStore['sExplosionMask'];
2196 sprStore.loadFont('sFontSmall');
2198 level.viewWidth = 320*3;
2199 level.viewHeight = 240*3;
2201 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2202 //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2203 fullscreen = global.config.startFullscreen;
2206 //SoundSystem.SwapStereo = config.swapStereo;
2207 SoundSystem.NumChannels = 32;
2208 SoundSystem.MaxHearingDistance = 12000;
2209 //SoundSystem.DopplerFactor = 1.0f;
2210 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2211 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2212 SoundSystem.ReferenceDistance = 16.0f*4;
2213 SoundSystem.MaxDistance = 16.0f*(5*10);
2215 SoundSystem.Initialize();
2216 if (!SoundSystem.IsInitialized) {
2217 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2218 global.soundDisabled = true;
2219 global.musicDisabled = true;
2221 global.fixVolumes();
2223 level.restartGame(); // this will NOT generate a new level
2228 texTigerEye = GLTexture.Load("teye0.png");
2230 if (global.cheatEndGameSequence) {
2231 level.winTime = 12*60+42;
2232 level.stats.money = 6666;
2233 switch (global.cheatEndGameSequence) {
2234 case 1: default: level.startWinCutscene(); break;
2235 case 2: level.startWinCutsceneVolcano(); break;
2236 case 3: level.startWinCutsceneWinFall(); break;
2239 switch (startMode) {
2240 case StartMode.Title: level.restartTitle(); break;
2241 case StartMode.Intro: level.restartIntro(); break;
2242 case StartMode.Stars: level.restartStarsRoom(); break;
2243 case StartMode.Sun: level.restartSunRoom(); break;
2244 case StartMode.Moon: level.restartMoonRoom(); break;
2246 level.generateNormalLevel();
2247 if (startMode == StartMode.Dead) {
2248 level.player.dead = true;
2249 level.player.visible = false;
2255 //global.rope = 666;
2256 //global.bombs = 666;
2258 //global.globalRoomSeed = 871520037;
2259 //global.globalOtherSeed = 1047036290;
2261 //level.createTitleRoom();
2262 //level.createTrans4Room();
2263 //level.createOlmecRoom();
2264 //level.generateLevel();
2266 //level.centerViewAtPlayer();
2267 teleportCameraAt(level.viewStart);
2268 //writeln(Video.swapInterval);
2270 Video.runEventLoop();
2271 Video.showMouseCursor();
2272 Video.closeScreen();
2273 SoundSystem.Shutdown();
2275 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2283 // ////////////////////////////////////////////////////////////////////////// //
2284 // duplicates are not allowed!
2285 final void checkGameObjNames () {
2286 array!(class!Object) known;
2288 int classCount = 0, namedCount = 0;
2289 foreach AllClasses(Object, out cc) {
2290 auto gn = GetClassGameObjName(cc);
2292 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2293 auto nid = NameToInt(gn);
2294 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));
2300 writeln(classCount, " classes, ", namedCount, " game object classes.");
2304 // ////////////////////////////////////////////////////////////////////////// //
2305 #include "timelimit.vc"
2306 //const int TimeLimitDate = 2018232;
2309 void performTimeCheck () {
2310 #ifdef DISABLE_TIME_CHECK
2312 if (TigerEye) return;
2315 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2318 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2320 int tldate = tm.year*1000+tm.yday;
2322 if (tldate > TimeLimitDate) {
2323 level.maxPlayingTime = 24;
2325 //writeln("*** days left: ", TimeLimitDate-tldate);
2331 void setupCheats () {
2334 //level.stats.resetTunnelPrices();
2335 startMode = StartMode.Alive;
2336 global.currLevel = 10;
2337 //global.scumGenAlienCraft = true;
2338 //global.scumGenYetiLair = true;
2341 startMode = StartMode.Alive;
2342 global.currLevel = 8;
2344 level.stats.tunnel1Left = level.stats.default.tunnel1Left;
2345 level.stats.tunnel2Left = level.stats.default.tunnel2Left;
2346 level.stats.tunnel1Active = false;
2347 level.stats.tunnel2Active = false;
2348 level.stats.tunnel3Active = false;
2352 startMode = StartMode.Alive;
2353 global.currLevel = 2;
2354 global.scumGenShop = true;
2355 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2356 //global.config.scale = 1;
2359 startMode = StartMode.Alive;
2360 global.currLevel = 13;
2361 global.config.scale = 2;
2364 startMode = StartMode.Alive;
2365 global.currLevel = 13;
2366 global.config.scale = 1;
2367 global.cityOfGold = true;
2370 startMode = StartMode.Alive;
2371 global.currLevel = 5;
2372 global.genBlackMarket = true;
2375 startMode = StartMode.Alive;
2376 global.currLevel = 2;
2377 global.scumGenShop = true;
2378 global.scumGenShopType = GameGlobal::ShopType.Weapon;
2379 //global.scumGenShopType = GameGlobal::ShopType.Craps;
2380 //global.config.scale = 1;
2383 //startMode = StartMode.Intro;
2386 global.currLevel = 2;
2387 startMode = StartMode.Alive;
2390 global.currLevel = 5;
2391 startMode = StartMode.Alive;
2392 global.scumGenLake = true;
2393 global.config.scale = 1;
2396 startMode = StartMode.Alive;
2397 global.cheatCanSkipOlmec = true;
2398 global.currLevel = 16;
2399 //global.currLevel = 5;
2400 //global.currLevel = 13;
2401 //global.config.scale = 1;
2403 //startMode = StartMode.Dead;
2404 //startMode = StartMode.Title;
2405 //startMode = StartMode.Stars;
2406 //startMode = StartMode.Sun;
2407 startMode = StartMode.Moon;
2409 //global.scumGenSacrificePit = true;
2410 //global.scumAlwaysSacrificeAltar = true;
2412 // first lush jungle level
2413 //global.levelType = 1;
2415 global.scumGenCemetary = true;
2417 //global.idol = false;
2418 //global.currLevel = 5;
2420 //global.isTunnelMan = true;
2423 //global.currLevel = 5;
2424 //global.scumGenLake = true;
2426 //global.currLevel = 5;
2427 //global.currLevel = 9;
2428 //global.currLevel = 13;
2429 //global.currLevel = 14;
2430 //global.cheatEndGameSequence = 1;
2433 //global.currLevel = 6;
2434 global.scumGenAlienCraft = true;
2435 global.currLevel = 9;
2436 //global.scumGenYetiLair = true;
2437 //global.genBlackMarket = true;
2438 //startDead = false;
2439 startMode = StartMode.Alive;
2442 global.cheatCanSkipOlmec = true;
2443 global.currLevel = 15;
2444 startMode = StartMode.Alive;
2447 global.scumGenShop = true;
2448 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2449 global.scumGenShopType = GameGlobal::ShopType.Craps;
2450 //global.scumGenShopType = 6; // craps
2451 //global.scumGenShopType = 7; // kissing
2453 //global.scumAlwaysSacrificeAltar = true;
2457 void setupSeeds () {
2461 // ////////////////////////////////////////////////////////////////////////// //
2463 checkGameObjNames();
2465 appSetName("k8spelunky");
2466 config = SpawnObject(GameConfig);
2467 global = SpawnObject(GameGlobal);
2468 global.config = config;
2469 config.heroType = GameConfig::Hero.Spelunker;
2471 global.randomizeSeedAll();
2473 fillCheatPickupList();
2474 fillCheatItemsList();
2475 fillCheatEnemiesList();
2478 loadKeyboardBindings();