1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2018, Ketmar Dark
5 * This file is part of Spelunky.
7 * You can redistribute and/or modify Spelunky, including its source code, under
8 * the terms of the Spelunky User License.
10 * Spelunky is distributed in the hope that it will be entertaining and useful,
11 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
13 * The Spelunky User License should be available in "Game Information", which
14 * can be found in the Resource Explorer, or as an external file called COPYING.
15 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17 **********************************************************************************/
25 //#define QUIT_DOUBLE_ESC
29 //#define BIGGER_REPLAY_DATA
31 // ////////////////////////////////////////////////////////////////////////// //
32 #include "mapent/0all.vc"
33 #include "PlayerPawn.vc"
34 #include "PlayerPowerup.vc"
35 #include "GameLevel.vc"
38 // ////////////////////////////////////////////////////////////////////////// //
39 #include "uisimple.vc"
42 // ////////////////////////////////////////////////////////////////////////// //
43 class DebugSessionMovement : Object;
45 #ifdef BIGGER_REPLAY_DATA
46 array!(GameLevel::SavedKeyState) keypresses;
48 array!ubyte keypresses; // on each frame
50 GameConfig playconfig;
53 transient int otherSeed, roomSeed;
56 override void Destroy () {
58 keypresses.length = 0;
63 final void resetReplay () {
68 #ifndef BIGGER_REPLAY_DATA
69 final void addKey (int kbidx, bool down) {
70 if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
71 keypresses[$] = kbidx|(down ? 0x80 : 0);
75 final void addEndOfFrame () {
86 final int getKey (out int kbidx, out bool down) {
87 if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
88 if (keypos >= keypresses.length) return END_OF_RECORD;
89 ubyte b = keypresses[keypos++];
90 if (b == 0xff) return END_OF_FRAME;
98 // ////////////////////////////////////////////////////////////////////////// //
99 class TempOptionsKeys : Object;
101 int[16*GameConfig::MaxActionBinds] keybinds;
104 // ////////////////////////////////////////////////////////////////////////// //
107 transient string dbgSessionStateFileName = "debug_game_session_state";
108 transient string dbgSessionMovementFileName = "debug_game_session_movement";
110 GLTexture texTigerEye;
114 SpriteStore sprStore;
115 BackTileStore bgtileStore;
118 int mouseX = int.min, mouseY = int.min;
119 int mouseLevelX = int.min, mouseLevelY = int.min;
120 bool renderMouseTile;
121 bool renderMouseRect;
132 StartMode startMode = StartMode.Title;
133 bool freeRide = false;
134 bool switchInterpolator;
137 bool replayFastForward = false;
138 int replayFastForwardSpeed = 2;
139 bool saveGameSession = false;
140 bool replayGameSession = false;
146 Replay doGameSavingPlaying = Replay.None;
147 float saveMovementLastTime = 0;
148 DebugSessionMovement debugMovement;
149 GameStats origStats; // for replaying
150 GameConfig origConfig; // for replaying
151 int origRoomSeed, origOtherSeed;
157 transient int maskSX, maskSY;
158 transient SpriteImage smask;
159 transient int maskFrame;
163 // ////////////////////////////////////////////////////////////////////////// //
164 final void saveKeyboardBindings () {
165 auto tok = SpawnObject(TempOptionsKeys);
166 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
167 appSaveOptions(tok, "keybindings");
172 final void loadKeyboardBindings () {
173 auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
175 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
181 // ////////////////////////////////////////////////////////////////////////// //
182 void saveGameOptions () {
183 appSaveOptions(global.config, "config");
187 void loadGameOptions () {
188 auto cfg = appLoadOptions(GameConfig, "config");
190 auto oldHero = config.heroType;
191 auto tok = SpawnObject(TempOptionsKeys);
192 foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
193 delete global.config;
196 foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
198 writeln("config loaded");
199 global.restartMusic();
201 //config.heroType = GameConfig::Hero.Spelunker;
202 config.heroType = oldHero;
205 if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
209 // ////////////////////////////////////////////////////////////////////////// //
210 void saveGameStats () {
211 if (level.stats) appSaveOptions(level.stats, "stats");
215 void loadGameStats () {
216 auto stats = appLoadOptions(GameStats, "stats");
221 if (!level.stats) level.stats = SpawnObject(GameStats);
222 level.stats.global = global;
226 // ////////////////////////////////////////////////////////////////////////// //
227 struct UIPaneSaveInfo {
229 UIPane::SaveInfo nfo;
232 transient UIPane optionsPane; // either options, or binding editor
234 transient GameLevel::IVec2D optionsPaneOfs;
235 transient void delegate () saveOptionsDG;
237 transient array!UIPaneSaveInfo optionsPaneState;
240 final void saveCurrentPane () {
241 if (!optionsPane || !optionsPane.id) return;
244 if (optionsPane.id == 'CheatFlags') {
245 if (instantGhost && level.ghostTimeLeft > 0) {
246 level.ghostTimeLeft = 1;
250 foreach (ref auto psv; optionsPaneState) {
251 if (psv.id == optionsPane.id) {
252 optionsPane.saveState(psv.nfo);
257 optionsPaneState.length += 1;
258 optionsPaneState[$-1].id = optionsPane.id;
259 optionsPane.saveState(optionsPaneState[$-1].nfo);
263 final void restoreCurrentPane () {
264 if (optionsPane) optionsPane.setupHotkeys(); // why not?
265 if (!optionsPane || !optionsPane.id) return;
266 foreach (ref auto psv; optionsPaneState) {
267 if (psv.id == optionsPane.id) {
268 optionsPane.restoreState(psv.nfo);
275 // ////////////////////////////////////////////////////////////////////////// //
276 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
277 if (!it.tagClass) return;
278 if (class!MapObject(it.tagClass)) {
279 level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
280 it.owner.closeMe = true;
285 // ////////////////////////////////////////////////////////////////////////// //
286 transient array!(class!MapObject) cheatItemsList;
289 final void fillCheatItemsList () {
290 cheatItemsList.length = 0;
291 cheatItemsList[$] = ItemProjectileArrow;
292 cheatItemsList[$] = ItemWeaponShotgun;
293 cheatItemsList[$] = ItemWeaponAshShotgun;
294 cheatItemsList[$] = ItemWeaponPistol;
295 cheatItemsList[$] = ItemWeaponMattock;
296 cheatItemsList[$] = ItemWeaponMachete;
297 cheatItemsList[$] = ItemWeaponWebCannon;
298 cheatItemsList[$] = ItemWeaponSceptre;
299 cheatItemsList[$] = ItemWeaponBow;
300 cheatItemsList[$] = ItemBones;
301 cheatItemsList[$] = ItemFakeBones;
302 cheatItemsList[$] = ItemFishBone;
303 cheatItemsList[$] = ItemRock;
304 cheatItemsList[$] = ItemJar;
305 cheatItemsList[$] = ItemSkull;
306 cheatItemsList[$] = ItemGoldenKey;
307 cheatItemsList[$] = ItemGoldIdol;
308 cheatItemsList[$] = ItemCrystalSkull;
309 cheatItemsList[$] = ItemShellSingle;
310 cheatItemsList[$] = ItemChest;
311 cheatItemsList[$] = ItemCrate;
312 cheatItemsList[$] = ItemLockedChest;
313 cheatItemsList[$] = ItemDice;
314 cheatItemsList[$] = ItemBasketBall;
318 final UIPane createCheatItemsPane () {
319 if (!level.player) return none;
321 UIPane pane = SpawnObject(UIPane);
323 pane.sprStore = sprStore;
325 pane.width = 320*3-64;
326 pane.height = 240*3-64;
328 foreach (auto ipk; cheatItemsList) {
329 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
333 //optionsPaneOfs.x = 100;
334 //optionsPaneOfs.y = 50;
340 // ////////////////////////////////////////////////////////////////////////// //
341 transient array!(class!MapObject) cheatEnemiesList;
344 final void fillCheatEnemiesList () {
345 cheatEnemiesList.length = 0;
346 cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
347 cheatEnemiesList[$] = EnemyBat;
348 cheatEnemiesList[$] = EnemySpiderHang;
349 cheatEnemiesList[$] = EnemySpider;
350 cheatEnemiesList[$] = EnemySnake;
351 cheatEnemiesList[$] = EnemyCaveman;
352 cheatEnemiesList[$] = EnemySkeleton;
353 cheatEnemiesList[$] = MonsterShopkeeper;
354 cheatEnemiesList[$] = EnemyZombie;
355 cheatEnemiesList[$] = EnemyVampire;
356 cheatEnemiesList[$] = EnemyFrog;
357 cheatEnemiesList[$] = EnemyGreenFrog;
358 cheatEnemiesList[$] = EnemyFireFrog;
359 cheatEnemiesList[$] = EnemyMantrap;
360 cheatEnemiesList[$] = EnemyScarab;
361 cheatEnemiesList[$] = EnemyFloater;
362 cheatEnemiesList[$] = EnemyBlob;
363 cheatEnemiesList[$] = EnemyMonkey;
364 cheatEnemiesList[$] = EnemyGoldMonkey;
365 cheatEnemiesList[$] = EnemyAlien;
366 cheatEnemiesList[$] = EnemyYeti;
367 cheatEnemiesList[$] = EnemyHawkman;
368 cheatEnemiesList[$] = EnemyUFO;
369 cheatEnemiesList[$] = EnemyYetiKing;
373 final UIPane createCheatEnemiesPane () {
374 if (!level.player) return none;
376 UIPane pane = SpawnObject(UIPane);
378 pane.sprStore = sprStore;
380 pane.width = 320*3-64;
381 pane.height = 240*3-64;
383 foreach (auto ipk; cheatEnemiesList) {
384 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
388 //optionsPaneOfs.x = 100;
389 //optionsPaneOfs.y = 50;
395 // ////////////////////////////////////////////////////////////////////////// //
396 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
399 final void fillCheatPickupList () {
400 cheatPickupList.length = 0;
401 cheatPickupList[$] = ItemPickupBombBag;
402 cheatPickupList[$] = ItemPickupBombBox;
403 cheatPickupList[$] = ItemPickupPaste;
404 cheatPickupList[$] = ItemPickupRopePile;
405 cheatPickupList[$] = ItemPickupShellBox;
406 cheatPickupList[$] = ItemPickupAnkh;
407 cheatPickupList[$] = ItemPickupCape;
408 cheatPickupList[$] = ItemPickupJetpack;
409 cheatPickupList[$] = ItemPickupUdjatEye;
410 cheatPickupList[$] = ItemPickupCrown;
411 cheatPickupList[$] = ItemPickupKapala;
412 cheatPickupList[$] = ItemPickupParachute;
413 cheatPickupList[$] = ItemPickupCompass;
414 cheatPickupList[$] = ItemPickupSpectacles;
415 cheatPickupList[$] = ItemPickupGloves;
416 cheatPickupList[$] = ItemPickupMitt;
417 cheatPickupList[$] = ItemPickupJordans;
418 cheatPickupList[$] = ItemPickupSpringShoes;
419 cheatPickupList[$] = ItemPickupSpikeShoes;
420 cheatPickupList[$] = ItemPickupTeleporter;
424 final UIPane createCheatPickupsPane () {
425 if (!level.player) return none;
427 UIPane pane = SpawnObject(UIPane);
429 pane.sprStore = sprStore;
431 pane.width = 320*3-64;
432 pane.height = 240*3-64;
434 foreach (auto ipk; cheatPickupList) {
435 auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
439 //optionsPaneOfs.x = 100;
440 //optionsPaneOfs.y = 50;
446 // ////////////////////////////////////////////////////////////////////////// //
447 transient int instantGhost;
449 final UIPane createCheatFlagsPane () {
450 UIPane pane = SpawnObject(UIPane);
451 pane.id = 'CheatFlags';
452 pane.sprStore = sprStore;
454 pane.width = 320*3-64;
455 pane.height = 240*3-64;
459 UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
460 UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
461 UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
462 UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
463 UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
464 //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
465 UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
466 UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
467 UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
468 UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
469 UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
470 UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
471 //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
472 UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
473 UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
474 UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
475 UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
476 UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
478 optionsPaneOfs.x = 100;
479 optionsPaneOfs.y = 50;
485 final UIPane createOptionsPane () {
486 UIPane pane = SpawnObject(UIPane);
488 pane.sprStore = sprStore;
490 pane.width = 320*3-64;
491 pane.height = 240*3-64;
495 //!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.");
498 UILabel.Create(pane, "VISUAL OPTIONS");
499 UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
500 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).");
501 // we don't have intro yet
502 //UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
503 UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
506 UILabel.Create(pane, "");
507 UILabel.Create(pane, "HUD OPTIONS");
508 UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
509 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.");
510 auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
513 auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
517 UILabel.Create(pane, "");
518 UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
519 //!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.");
520 //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
521 UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
522 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.");
523 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.");
524 UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
525 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.");
526 UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
529 UILabel.Create(pane, "");
530 UILabel.Create(pane, "GAMEPLAY OPTIONS");
531 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.");
532 UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
533 UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
534 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!");
535 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.");
536 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.");
537 UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
538 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.");
539 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.");
540 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.");
541 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.");
542 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?");
543 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.");
544 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.");
545 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.");
546 UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
547 UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
548 UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
549 auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
550 rstl.names[$] = "RANDOM";
551 rstl.names[$] = "NORMAL";
552 rstl.names[$] = "BIZARRE";
555 UILabel.Create(pane, "");
556 UILabel.Create(pane, "WHIP OPTIONS");
557 UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
558 auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
559 whiptype.names[$] = "NORMAL";
560 whiptype.names[$] = "LONG";
561 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.");
564 UILabel.Create(pane, "");
565 UILabel.Create(pane, "PLAYER OPTIONS");
566 auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
567 herotype.names[$] = "SPELUNKY GUY";
568 herotype.names[$] = "DAMSEL";
569 herotype.names[$] = "TUNNEL MAN";
572 UILabel.Create(pane, "");
573 UILabel.Create(pane, "CHEAT OPTIONS");
574 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.");
575 auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
576 plrlit.names[$] = "NEVER";
577 plrlit.names[$] = "FORCED DARKNESS";
578 plrlit.names[$] = "ALWAYS";
579 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'.");
580 rdark.names[$] = "NEVER";
581 rdark.names[$] = "DEFAULT";
582 rdark.names[$] = "ALWAYS";
583 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.");
585 rghost.getNameCB = delegate string (int val) {
586 if (val < 0) return "INSTANT";
587 if (val == 0) return "NEVER";
588 if (val < 120) return va("%d SEC", val);
589 if (val%60 == 0) return va("%d MIN", val/60);
590 if (val%60 == 30) return va("%d.5 MIN", val/60);
591 return va("%d MIN, %d SEC", val/60, val%60);
593 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.");
595 UILabel.Create(pane, "");
596 UILabel.Create(pane, "CHEAT START OPTIONS");
597 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.");
598 UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
599 UIIntEnum.Create(pane, &config.scumStartLife, 1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
600 UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
601 UIIntEnum.Create(pane, &config.scumStartRope, 1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
604 UILabel.Create(pane, "");
605 UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
606 auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
607 mm.names[$] = "SILENCE";
608 mm.names[$] = "RESTART";
609 mm.names[$] = "DON'T TOUCH";
611 mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
612 //mm.names[$] = "SILENCE";
613 mm.names[$] = "RESTART";
614 mm.names[$] = "DON'T TOUCH";
617 //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
619 swstereo.onValueChanged = delegate void (int newval) {
620 SoundSystem.SwapStereo = newval;
624 UILabel.Create(pane, "");
625 UILabel.Create(pane, "SOUND CONTROL CENTER");
626 auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
627 rmusonoff.onValueChanged = delegate void (int newval) {
628 global.restartMusic();
631 UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
633 auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
634 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
636 rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
637 rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
640 saveOptionsDG = delegate void () {
641 writeln("saving options");
644 optionsPaneOfs.x = 42;
645 optionsPaneOfs.y = 0;
651 final void createBindingsControl (UIPane pane, int keyidx) {
654 case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
655 case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
656 case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
657 case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
658 case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
659 case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
660 case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
661 case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
662 case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
663 case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
664 case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
667 int arridx = GameConfig.getKeyIndex(keyidx);
668 UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
672 final UIPane createBindingsPane () {
673 UIPane pane = SpawnObject(UIPane);
674 pane.id = 'KeyBindings';
675 pane.sprStore = sprStore;
677 pane.width = 320*3-64;
678 pane.height = 240*3-64;
680 createBindingsControl(pane, GameConfig::Key.Left);
681 createBindingsControl(pane, GameConfig::Key.Right);
682 createBindingsControl(pane, GameConfig::Key.Up);
683 createBindingsControl(pane, GameConfig::Key.Down);
684 createBindingsControl(pane, GameConfig::Key.Jump);
685 createBindingsControl(pane, GameConfig::Key.Run);
686 createBindingsControl(pane, GameConfig::Key.Attack);
687 createBindingsControl(pane, GameConfig::Key.Switch);
688 createBindingsControl(pane, GameConfig::Key.Pay);
689 createBindingsControl(pane, GameConfig::Key.Bomb);
690 createBindingsControl(pane, GameConfig::Key.Rope);
692 saveOptionsDG = delegate void () {
693 writeln("saving keys");
694 saveKeyboardBindings();
696 optionsPaneOfs.x = 120;
697 optionsPaneOfs.y = 140;
703 // ////////////////////////////////////////////////////////////////////////// //
704 void clearGameMovement () {
705 debugMovement = SpawnObject(DebugSessionMovement);
706 debugMovement.playconfig = SpawnObject(GameConfig);
707 debugMovement.playconfig.copyGameplayConfigFrom(config);
708 debugMovement.resetReplay();
712 void saveGameMovement (string fname, optional bool packit) {
713 if (debugMovement) appSaveOptions(debugMovement, fname, packit);
714 saveMovementLastTime = GetTickCount();
718 void loadGameMovement (string fname) {
719 delete debugMovement;
720 debugMovement = appLoadOptions(DebugSessionMovement, fname);
721 debugMovement.resetReplay();
724 origStats = level.stats;
725 origStats.global = none;
726 level.stats = SpawnObject(GameStats);
727 level.stats.global = global;
730 config = debugMovement.playconfig;
731 global.config = config;
732 origRoomSeed = global.globalRoomSeed;
733 origOtherSeed = global.globalOtherSeed;
734 writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
739 void stopReplaying () {
741 writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
742 global.globalRoomSeed = origRoomSeed;
743 global.globalOtherSeed = origOtherSeed;
745 delete debugMovement;
746 saveGameSession = false;
747 replayGameSession = false;
748 doGameSavingPlaying = Replay.None;
751 origStats.global = global;
752 level.stats = origStats;
758 global.config = origConfig;
764 // ////////////////////////////////////////////////////////////////////////// //
765 final bool saveGame (string gmname) {
766 return appSaveOptions(level, gmname);
770 final bool loadGame (string gmname) {
771 auto olddel = ImmediateDelete;
772 ImmediateDelete = false;
774 auto stats = level.stats;
777 auto lvl = appLoadOptions(GameLevel, gmname);
779 //lvl.global.config = config;
784 global = level.global;
785 global.config = config;
787 level.sprStore = sprStore;
788 level.bgtileStore = bgtileStore;
791 level.onBeforeFrame = &beforeNewFrame;
792 level.onAfterFrame = &afterNewFrame;
793 level.onInterFrame = &interFrame;
794 level.onLevelExitedCB = &levelExited;
795 level.onCameraTeleported = &cameraTeleportedCB;
797 level.viewWidth = Video.screenWidth;
798 level.viewHeight = Video.screenHeight;
801 level.centerViewAtPlayer();
802 teleportCameraAt(level.viewStart);
804 recalcCameraCoords(0);
809 level.stats.global = level.global;
811 ImmediateDelete = olddel;
812 CollectGarbage(true); // destroy delayed objects too
817 // ////////////////////////////////////////////////////////////////////////// //
818 float lastThinkerTime;
819 int replaySkipFrame = 0;
822 final void onTimePasses () {
823 float curTime = GetTickCount();
824 if (lastThinkerTime > 0) {
825 if (curTime < lastThinkerTime) {
826 writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
827 lastThinkerTime = curTime;
830 if (replayFastForward && replaySkipFrame) {
832 lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
835 level.processThinkers(curTime-lastThinkerTime);
837 lastThinkerTime = curTime;
841 final void resetFramesAndForceOne () {
842 float curTime = GetTickCount();
843 lastThinkerTime = curTime;
845 auto wasPaused = level.gamePaused;
846 level.gamePaused = false;
847 if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
848 level.processThinkers(GameLevel::FrameTime);
849 level.gamePaused = wasPaused;
850 //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
854 // ////////////////////////////////////////////////////////////////////////// //
855 private float currFrameDelta; // so level renderer can properly interpolate the player
856 private GameLevel::IVec2D camPrev, camCurr;
857 private GameLevel::IVec2D camShake;
858 private GameLevel::IVec2D viewCameraPos;
861 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
866 viewCameraPos.x = pos.x;
867 viewCameraPos.y = pos.y;
873 // call `recalcCameraCoords()` to get real camera coords after this
874 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
875 // check if camera is moved too far, and teleport it
877 (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
878 abs(camCurr.y-pos.y)/global.scale >= 16*4))
880 teleportCameraAt(pos);
882 camPrev.x = camCurr.x;
883 camPrev.y = camCurr.y;
887 camShake.x = level.shakeDir.x*global.scale;
888 camShake.y = level.shakeDir.y*global.scale;
892 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
893 currFrameDelta = frameDelta;
894 viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
895 viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
897 // update sound listener position (it is either at player position, or in viewport center)
901 (viewCameraPos.x+level.viewWidth/2.0)/global.scale,
902 (viewCameraPos.y+level.viewHeight/2.0)/global.scale
904 SoundSystem.ListenerOrigin = lv;
906 viewCameraPos.x += camShake.x;
907 viewCameraPos.y += camShake.y;
908 //lv = vector(float(level.player.xCenter), float(level.player.yCenter));
910 //SoundSystem.ListenerOrigin = lv;
911 //SoundSystem.UpdateSounds(moveSounds ? 1 : 0);
915 GameLevel::SavedKeyState savedKeyState;
917 final void pauseGame () {
918 if (!level.gamePaused) {
919 if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
920 level.gamePaused = true;
921 global.pauseAllSounds();
926 final void unpauseGame () {
927 if (level.gamePaused) {
928 if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
929 level.gamePaused = false;
930 //lastThinkerTime = 0;
931 global.resumeAllSounds();
937 final void beforeNewFrame (bool frameSkip) {
939 level.disablePlayerThink = true;
942 if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
943 if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
944 if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
946 if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
947 if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
948 if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
949 if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
951 level.disablePlayerThink = false;
955 if (level.isKeyDown(PlayerPawn::KeyLeft)) level.player.fltx -= delta;
956 if (level.isKeyDown(PlayerPawn::KeyRight)) level.player.fltx += delta;
957 if (level.isKeyDown(PlayerPawn::KeyUp)) level.player.flty -= delta;
958 if (level.isKeyDown(PlayerPawn::KeyDown)) level.player.flty += delta;
961 if (!level.gamePaused) {
962 // save seeds for afterframe processing
964 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
965 debugMovement.otherSeed = global.globalOtherSeed;
966 debugMovement.roomSeed = global.globalRoomSeed;
970 if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
972 #ifdef BIGGER_REPLAY_DATA
973 if (doGameSavingPlaying == Replay.Saving && debugMovement) {
974 debugMovement.keypresses.length += 1;
975 level.keysSaveState(debugMovement.keypresses[$-1]);
976 debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
977 debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
981 if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
982 #ifdef BIGGER_REPLAY_DATA
983 if (debugMovement.keypos < debugMovement.keypresses.length) {
984 level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
985 global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
986 global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
987 ++debugMovement.keypos;
993 auto code = debugMovement.getKey(out kbidx, out down);
994 if (code == DebugSessionMovement::END_OF_RECORD) {
995 // do this in main loop, so we can view totals
999 if (code == DebugSessionMovement::END_OF_FRAME) {
1002 if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1003 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1011 final void afterNewFrame (bool frameSkip) {
1012 if (!replayFastForward) replaySkipFrame = 0;
1014 if (level.gamePaused) return;
1016 if (!level.gamePaused) {
1017 if (doGameSavingPlaying != Replay.None) {
1018 if (doGameSavingPlaying == Replay.Saving) {
1019 replayFastForward = false; // just in case
1020 #ifndef BIGGER_REPLAY_DATA
1021 debugMovement.addEndOfFrame();
1023 auto stt = GetTickCount();
1024 if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
1025 } else if (doGameSavingPlaying == Replay.Replaying) {
1026 if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1027 replaySkipFrame = 1;
1033 //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1034 //SoundSystem.UpdateSounds();
1036 if (!freeRide) level.fixCamera();
1037 setNewCameraPos(level.viewStart);
1039 prevCameraX = currCameraX;
1040 prevCameraY = currCameraY;
1041 currCameraX = level.cameraX;
1042 currCameraY = level.cameraY;
1043 // disable camera interpolation if the screen is shaking
1044 if (level.shakeX|level.shakeY) {
1045 prevCameraX = currCameraX;
1046 prevCameraY = currCameraY;
1049 // disable camera interpolation if it moves too far away
1050 if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1051 if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1053 if (switchInterpolator) {
1054 switchInterpolator = false;
1055 config.interpolateMovement = !config.interpolateMovement;
1057 recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1059 if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1060 pauseRequested = false;
1062 if (!showHelp) showHelp = true;
1068 final void interFrame (float frameDelta) {
1069 if (!config.interpolateMovement) return;
1070 recalcCameraCoords(frameDelta);
1074 final void cameraTeleportedCB () {
1075 teleportCameraAt(level.viewStart);
1076 recalcCameraCoords(0);
1080 // ////////////////////////////////////////////////////////////////////////// //
1082 final void setColorByIdx (bool isset, int col) {
1084 // missed collision: red
1085 Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1086 } else if (col == -999) {
1087 // superfluous collision: blue
1088 Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1089 } else if (col <= 0) {
1090 // no collision: yellow
1091 Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1092 } else if (col > 0) {
1094 Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1099 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1101 CollisionMask cm = CollisionMask.Create(frm, false);
1103 int scale = global.config.scale;
1104 int bx0, by0, bx1, by1;
1105 frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1106 Video.color = 0x7f_00_00_ff;
1107 Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1108 if (!cm.isEmptyMask) {
1109 //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1110 foreach (int iy; 0..cm.height) {
1111 foreach (int ix; 0..cm.width) {
1112 int v = cm.mask[ix, iy];
1113 foreach (int dx; 0..32) {
1116 Video.color = 0x3f_00_ff_00;
1117 Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1126 foreach (int iy; 0..frm.tex.height) {
1127 foreach (int ix; 0..(frm.tex.width+31)/31) {
1128 foreach (int dx; 0..32) {
1130 //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1131 if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1132 setColorByIdx(true, col);
1133 if (col <= 0) Video.color = 0xaf_ff_ff_00;
1135 Video.color = 0xaf_00_ff_00;
1137 Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1143 if (frm.bw > 0 && frm.bh > 0) {
1144 setColorByIdx(true, col);
1145 Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1146 Video.color = 0xff_00_00;
1147 Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1156 // ////////////////////////////////////////////////////////////////////////// //
1157 transient int drawStats;
1158 transient array!int statsTopItem;
1161 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1162 auto sa = string(a.objName);
1163 auto sb = string(b.objName);
1168 final int getStatsTopItem () {
1169 return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1173 final void setStatsTopItem (int val) {
1174 if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1175 statsTopItem[drawStats] = val;
1179 final void resetStatsTopItem () {
1184 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1185 sprStore.loadFont('sFontSmall');
1191 final int calcStatsVisItems () {
1194 statsDrawGetStartPosLoadFont(currX, currY);
1195 int endY = Video.screenHeight-(currY*2);
1196 return max(1, endY/sprStore.getFontHeight(scale));
1200 int getStatsItemCount () {
1201 switch (drawStats) {
1202 case 2: return level.stats.totalKills.length;
1203 case 3: return level.stats.totalDeaths.length;
1204 case 4: return level.stats.totalCollected.length;
1210 final void statsMoveUp () {
1211 int count = getStatsItemCount();
1212 if (count < 0) return;
1213 int visItems = calcStatsVisItems();
1214 if (count <= visItems) { resetStatsTopItem(); return; }
1215 int top = getStatsTopItem();
1217 setStatsTopItem(top-1);
1221 final void statsMoveDown () {
1222 int count = getStatsItemCount();
1223 if (count < 0) return;
1224 int visItems = calcStatsVisItems();
1225 if (count <= visItems) { resetStatsTopItem(); return; }
1226 int top = getStatsTopItem();
1227 //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1228 top = clamp(top+1, 0, count-visItems);
1229 setStatsTopItem(top);
1233 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1234 arr.sort(&totalsNameCmpCB);
1238 statsDrawGetStartPosLoadFont(currX, currY);
1240 int endY = Video.screenHeight-(currY*2);
1241 int visItems = calcStatsVisItems();
1243 if (arr.length <= visItems) resetStatsTopItem();
1245 int topItem = getStatsTopItem();
1249 Video.color = 0x3f_ff_ff_00;
1250 auto spr = sprStore['sPageUp'];
1251 spr.frames[0].tex.blitAt(currX-24, currY, scale);
1254 // "downscroll" mark
1255 if (topItem+visItems < arr.length) {
1256 Video.color = 0x3f_ff_ff_00;
1257 auto spr = sprStore['sPageDown'];
1258 spr.frames[0].tex.blitAt(currX-24, endY/*-sprStore.getFontHeight(scale)*/, scale);
1261 Video.color = 0xff_ff_00;
1262 int hiColor = 0x00_ff_00;
1263 int hiColor1 = 0xf_ff_ff;
1266 while (it < arr.length && visItems-- > 0) {
1267 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);
1268 currY += sprStore.getFontHeight(scale);
1274 void drawStatsScreen () {
1275 int deathCount, killCount, collectCount;
1277 sprStore.loadFont('sFontSmall');
1278 Video.color = 0xff_ff_00;
1279 int hiColor = 0x00_ff_00;
1281 switch (drawStats) {
1282 case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1283 case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1284 case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1287 if (drawStats > 1) {
1289 foreach (ref auto i; statsTopItem) i = 0;
1294 foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1295 foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1296 foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1302 sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1303 currY += sprStore.getFontHeight(scale);
1305 int gw = level.stats.gamesWon;
1306 sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1307 currY += sprStore.getFontHeight(scale);
1309 sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1310 currY += sprStore.getFontHeight(scale);
1312 sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1313 currY += sprStore.getFontHeight(scale);
1315 sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1316 currY += sprStore.getFontHeight(scale);
1318 sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1319 currY += sprStore.getFontHeight(scale);
1321 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1322 currY += sprStore.getFontHeight(scale);
1324 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1325 currY += sprStore.getFontHeight(scale);
1327 sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1328 currY += sprStore.getFontHeight(scale);
1330 sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1331 currY += sprStore.getFontHeight(scale);
1333 int gs = level.stats.totalGhostSummoned;
1334 sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1335 currY += sprStore.getFontHeight(scale);
1337 currY += sprStore.getFontHeight(scale);
1338 sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1339 currY += sprStore.getFontHeight(scale);
1344 if (Video.frameTime == 0) {
1346 Video.requestRefresh();
1351 if (level.framesProcessedFromLastClear < 1) return;
1352 calcMouseMapCoords();
1354 Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1355 Video.clearScreen();
1356 Video.stencil = false;
1357 Video.color = 0xff_ff_ff;
1358 Video.textureFiltering = false;
1359 // don't touch framebuffer alpha
1360 Video.colorMask = Video::CMask.Colors;
1362 level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1364 if (level.gamePaused) {
1365 if (mouseLevelX != int.min) {
1366 int scale = level.global.scale;
1367 if (renderMouseRect) {
1368 Video.color = 0xcf_ff_ff_00;
1369 Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1371 if (renderMouseTile) {
1372 Video.color = 0xaf_ff_00_00;
1373 Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1378 switch (doGameSavingPlaying) {
1380 Video.color = 0x7f_00_ff_00;
1381 sprStore.loadFont('sFont');
1382 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1384 case Replay.Replaying:
1385 if (level.player && !level.player.dead) {
1386 Video.color = 0x7f_ff_00_00;
1387 sprStore.loadFont('sFont');
1388 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1389 int th = sprStore.getFontHeight(2);
1390 if (replayFastForward) {
1391 sprStore.loadFont('sFontSmall');
1392 string sstr = va("x%d", replayFastForwardSpeed+1);
1393 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1398 if (saveGameSession) {
1399 Video.color = 0x7f_ff_7f_00;
1400 sprStore.loadFont('sFont');
1401 sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1407 if (level.player && level.player.dead && !showHelp) {
1409 Video.color = 0x8f_00_00_00;
1410 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1415 if (true /*level.inWinCutscene == 0*/) {
1416 Video.color = 0xff_ff_ff;
1417 sprStore.loadFont('sFontSmall');
1418 string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1420 "PRESS $PAY TO RESTART GAME\n"~
1422 "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1424 "TOTAL PLAYING TIME: |%s|"~
1426 (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1427 level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1428 level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1430 GameLevel.time2str(level.stats.playingTime)
1432 kmsg = global.expandString(kmsg);
1433 sprStore.renderMultilineTextCentered(Video.screenWidth/2, int.min, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1440 Video.color = 0xff_7f_00;
1441 sprStore.loadFont('sFontSmall');
1442 sprStore.renderText(8, Video.screenHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1443 auto spf = smask.frames[maskFrame];
1444 sprStore.renderText(8, Video.screenHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1446 spf.bx, spf.by, spf.bw, spf.bh,
1447 (spf.maskEmpty ? "TAN" : "ONA"),
1448 (spf.precise ? "TAN" : "ONA")),
1451 //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1452 //writeln("pos=(", maskSX, ",", maskSY, ")");
1453 int scale = global.config.scale;
1454 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1455 int mapX = xofs/scale+maskSX;
1456 int mapY = yofs/scale+maskSY;
1459 writeln("==== tiles ====");
1461 level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1462 if (t.spectral || !t.isInstanceAlive) return false;
1463 Video.color = 0x7f_ff_00_00;
1464 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);
1465 auto tsf = t.getSpriteFrame();
1467 auto spf = smask.frames[maskFrame];
1468 int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1469 int mapX = xofs/global.config.scale+maskSX;
1470 int mapY = yofs/global.config.scale+maskSY;
1473 //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1474 bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1475 writeln(" tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1479 level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1480 Video.color = 0x7f_ff_00_00;
1481 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);
1485 drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1487 Video.color = 0xaf_ff_ff_ff;
1488 spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1489 Video.color = 0xff_ff_00;
1490 Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1494 int fx0, fy0, fx1, fy1;
1495 auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1496 Video.color = 0x7f_00_00_ff;
1497 Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1503 Video.color = 0x8f_00_00_00;
1504 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1506 optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1511 Video.color = 0xff_ff_00;
1512 //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1513 if (showHelp == 1) {
1514 sprStore.loadFont('sFontSmall');
1515 sprStore.renderTextWrapped(16, 16, (320-16)*2,
1516 "F1: show this help\n"~
1518 "K : redefine keys\n"~
1519 "I : toggle interpolaion\n"~
1520 "N : create some blood\n"~
1521 "R : generate a new level\n"~
1522 "F : toggle \"Frozen Area\"\n"~
1523 "X : resurrect player\n"~
1524 "Q : teleport to exit\n"~
1525 "D : teleport to damel\n"~
1527 "C : cheat flags menu\n"~
1528 "P : cheat pickup menu\n"~
1529 "E : cheat enemy menu\n"~
1530 "Enter: cheat items menu\n"~
1532 "TAB: toggle 'freeroam' mode\n"~
1536 if (level) level.renderPauseOverlay();
1540 //SoundSystem.UpdateSounds();
1542 //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1545 Video.color = 0xaf_ff_ff_ff;
1546 texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1551 // ////////////////////////////////////////////////////////////////////////// //
1552 transient bool gameJustOver;
1553 transient bool waitingForPayRestart;
1556 final void calcMouseMapCoords () {
1557 if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1558 mouseLevelX = int.min;
1559 mouseLevelY = int.min;
1562 mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1563 mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1564 //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1568 final void onEvent (ref event_t evt) {
1569 if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1571 if (evt.type == ev_winfocus) {
1572 if (level && !evt.focused) {
1577 //writeln("FOCUS!");
1578 Video.getMousePos(out mouseX, out mouseY);
1583 if (evt.type == ev_mouse) {
1586 calcMouseMapCoords();
1589 if (evt.type == ev_keydown) {
1590 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1591 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1592 renderMouseTile = evt.bCtrl;
1593 renderMouseRect = evt.bAlt;
1596 if (evt.type == ev_keyup) {
1597 if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1598 if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1599 renderMouseTile = evt.bCtrl;
1600 renderMouseRect = evt.bAlt;
1603 if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1605 if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1606 int newScale = evt.keycode-48;
1607 if (global.config.scale != newScale) {
1608 global.config.scale = newScale;
1611 cameraTeleportedCB();
1618 if (evt.type == ev_mouse) {
1619 maskSX = evt.x/global.config.scale;
1620 maskSY = evt.y/global.config.scale;
1623 if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1624 maskFrame = max(0, maskFrame-1);
1627 if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1628 maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1637 if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1639 if (saveOptionsDG) saveOptionsDG();
1640 saveOptionsDG = none;
1642 //SoundSystem.UpdateSounds(); // just in case
1643 if (global.hasSpectacles) level.pickedSpectacles();
1646 optionsPane.onEvent(evt);
1650 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1651 if (evt.type == ev_keydown) {
1652 switch (evt.keycode) {
1653 case K_F1: if (showHelp > 1) showHelp = 1; else unpauseGame(); return;
1654 case K_F10: Video.requestQuit(); return;
1655 case K_F12: showHelp = 3-showHelp; return;
1657 case K_UPARROW: case K_PAD8:
1658 if (drawStats) statsMoveUp();
1660 case K_DOWNARROW: case K_PAD2:
1661 if (drawStats) statsMoveDown();
1674 resetFramesAndForceOne();
1688 case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1689 case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1690 case K_c: optionsPane = createCheatFlagsPane(); restoreCurrentPane(); return;
1691 case K_p: optionsPane = createCheatPickupsPane(); restoreCurrentPane(); return;
1692 case K_ENTER: optionsPane = createCheatItemsPane(); restoreCurrentPane(); return;
1693 case K_e: optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); return;
1694 case K_TAB: freeRide = !freeRide; return;
1695 //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1696 //case K_j: global.hasJordans = !global.hasJordans; return;
1697 case K_i: switchInterpolator = true; unpauseGame(); return;
1701 auto bomb = ItemBomb(level.MakeMapObject(level.player.ix, level.player.iy, 'oBomb'));
1702 if (bomb) bomb.armIt();
1704 level.resurrectPlayer();
1709 //writeln("*** ROOM SEED: ", global.globalRoomSeed);
1710 //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1711 if (evt.bAlt && level.player && level.player.dead) {
1712 saveGameSession = false;
1713 replayGameSession = true;
1717 if (evt.bCtrl) global.idol = false;
1718 level.generateLevel();
1719 level.centerViewAtPlayer();
1720 teleportCameraAt(level.viewStart);
1721 resetFramesAndForceOne();
1724 global.toggleMusic();
1727 level.pickedSpectacles();
1730 global.config.useFrozenRegion = !global.config.useFrozenRegion;
1734 if (level.allExits.length) {
1735 level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1741 auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1743 level.teleportPlayerTo(damsel.ix, damsel.iy);
1750 auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1752 level.teleportPlayerTo(obj.ix, obj.iy-4);
1759 auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1761 level.teleportPlayerTo(obj.ix, obj.iy);
1768 if (level && mouseLevelX != int.min) {
1769 int scale = level.global.scale;
1770 int mapX = mouseLevelX;
1771 int mapY = mouseLevelY;
1772 level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1777 if (level && mouseLevelX != int.min) {
1778 int scale = level.global.scale;
1779 int mapX = mouseLevelX;
1780 int mapY = mouseLevelY;
1783 writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1784 level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1785 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1789 foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1790 writeln(" tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1796 auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1797 //if (obj) obj.monkey = monkey;
1799 //playSound('sndThump');
1805 case K_DELETE: // suicide
1806 if (doGameSavingPlaying == Replay.None) {
1807 if (level.player && !level.player.dead && evt.bCtrl) {
1808 global.hasAnkh = false;
1809 level.global.plife = 1;
1810 level.player.invincible = 0;
1811 level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion');
1818 if (level.player && !level.player.dead && evt.bAlt) {
1819 if (doGameSavingPlaying != Replay.None) {
1820 if (doGameSavingPlaying == Replay.Replaying) {
1822 } else if (doGameSavingPlaying == Replay.Saving) {
1823 saveGameMovement(dbgSessionMovementFileName, packit:true);
1825 doGameSavingPlaying = Replay.None;
1827 saveGameSession = false;
1828 replayGameSession = false;
1835 level.stats.setMoneyCheat();
1836 level.stats.addMoney(10000);
1841 if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1842 if (level.player && level.player.dead) {
1843 //Video.requestQuit();
1845 if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1847 #ifdef QUIT_DOUBLE_ESC
1848 if (++escCount == 2) Video.requestQuit();
1851 pauseRequested = true;
1856 if (evt.type == ev_keydown && evt.keycode == K_F1 && evt.bShift) { pauseRequested = true; return; }
1859 if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1862 if (!level.player || !level.player.dead) {
1863 gameJustOver = false;
1864 } else if (level.player && level.player.dead) {
1865 if (!gameJustOver) {
1867 gameJustOver = true;
1868 waitingForPayRestart = true;
1869 level.clearKeysPressRelease();
1870 if (doGameSavingPlaying == Replay.None) {
1871 stopReplaying(); // just in case
1875 replayFastForward = false;
1876 if (doGameSavingPlaying == Replay.Saving) {
1877 if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
1878 doGameSavingPlaying = Replay.None;
1879 //clearGameMovement();
1880 saveGameSession = false;
1881 replayGameSession = false;
1884 if (evt.type == ev_keydown || evt.type == ev_keyup) {
1885 bool down = (evt.type == ev_keydown);
1886 if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
1887 if (down && evt.keycode == K_f) {
1889 if (replayFastForwardSpeed != 4) {
1890 replayFastForwardSpeed = 4;
1891 replayFastForward = true;
1893 replayFastForward = !replayFastForward;
1896 replayFastForwardSpeed = 2;
1897 replayFastForward = !replayFastForward;
1901 if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
1902 foreach (int kbidx, int kval; global.config.keybinds) {
1903 if (kval && kval == evt.keycode) {
1904 #ifndef BIGGER_REPLAY_DATA
1905 if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
1907 level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1911 if (level.player && level.player.dead) {
1912 if (down && evt.keycode == K_r && evt.bAlt) {
1913 saveGameSession = false;
1914 replayGameSession = true;
1917 if (down && evt.keycode == K_s && evt.bAlt) {
1918 bool wasSaveReq = saveGameSession;
1919 stopReplaying(); // just in case
1920 saveGameSession = !wasSaveReq;
1921 replayGameSession = false;
1924 if (replayGameSession) {
1925 stopReplaying(); // just in case
1926 saveGameSession = false;
1927 replayGameSession = false;
1928 loadGameMovement(dbgSessionMovementFileName);
1929 loadGame(dbgSessionStateFileName);
1930 doGameSavingPlaying = Replay.Replaying;
1932 if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
1933 if (waitingForPayRestart) {
1934 level.isKeyReleased(GameConfig::Key.Pay);
1935 if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
1937 level.isKeyPressed(GameConfig::Key.Pay);
1938 if (level.isKeyReleased(GameConfig::Key.Pay)) {
1939 auto doSave = saveGameSession;
1940 stopReplaying(); // just in case
1941 level.clearKeysPressRelease();
1942 level.restartGame();
1943 level.generateNormalLevel();
1945 saveGameSession = false;
1946 replayGameSession = false;
1947 writeln("DBG: saving game session...");
1948 clearGameMovement();
1949 doGameSavingPlaying = Replay.Saving;
1950 saveGame(dbgSessionStateFileName);
1951 //saveGameMovement(dbgSessionMovementFileName);
1962 void levelExited () {
1968 final void runGameLoop () {
1969 Video.frameTime = 0; // unlimited FPS
1970 lastThinkerTime = 0;
1972 sprStore = SpawnObject(SpriteStore);
1973 sprStore.bDumpLoaded = false;
1975 bgtileStore = SpawnObject(BackTileStore);
1976 bgtileStore.bDumpLoaded = false;
1978 level = SpawnObject(GameLevel);
1979 level.setup(global, sprStore, bgtileStore);
1981 level.BuildYear = BuildYear;
1982 level.BuildMonth = BuildMonth;
1983 level.BuildDay = BuildDay;
1984 level.BuildHour = BuildHour;
1985 level.BuildMin = BuildMin;
1987 level.global = global;
1988 level.sprStore = sprStore;
1989 level.bgtileStore = bgtileStore;
1993 level.onBeforeFrame = &beforeNewFrame;
1994 level.onAfterFrame = &afterNewFrame;
1995 level.onInterFrame = &interFrame;
1996 level.onLevelExitedCB = &levelExited;
1997 level.onCameraTeleported = &cameraTeleportedCB;
2000 maskSX = -0x0ff_fff;
2002 smask = sprStore['sExplosionMask'];
2006 sprStore.loadFont('sFontSmall');
2008 Video.swapInterval = (global.config.optVSync ? 1 : 0);
2009 Video.openScreen("Spelunky/VaVoom C", 320*3, 240*3);
2011 if (Video.realStencilBits < 8) {
2012 Video.closeScreen();
2013 FatalError("FATAL: no stencil buffer!");
2015 if (!Video.framebufferHasAlpha) {
2016 Video.closeScreen();
2017 FatalError("FATAL: no alpha channel in framebuffer!");
2020 //SoundSystem.SwapStereo = config.swapStereo;
2021 SoundSystem.NumChannels = 32;
2022 SoundSystem.MaxHearingDistance = 12000;
2023 //SoundSystem.DopplerFactor = 1.0f;
2024 //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2025 SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2026 SoundSystem.ReferenceDistance = 16.0f*4;
2027 SoundSystem.MaxDistance = 16.0f*(5*10);
2029 SoundSystem.Initialize();
2030 if (!SoundSystem.IsInitialized) {
2031 writeln("WARNING: cannot initialize sound system, turning off sound and music");
2032 global.soundDisabled = true;
2033 global.musicDisabled = true;
2035 global.fixVolumes();
2037 level.viewWidth = Video.screenWidth;
2038 level.viewHeight = Video.screenHeight;
2040 level.restartGame(); // this will NOT generate a new level
2045 texTigerEye = GLTexture.Load("sprites/teye0.png");
2047 if (global.cheatEndGameSequence) {
2048 level.winTime = 12*60+42;
2049 level.stats.money = 6666;
2050 switch (global.cheatEndGameSequence) {
2051 case 1: default: level.startWinCutscene(); break;
2052 case 2: level.startWinCutsceneVolcano(); break;
2053 case 3: level.startWinCutsceneWinFall(); break;
2056 switch (startMode) {
2057 case StartMode.Title: level.restartTitle(); break;
2058 case StartMode.Stars: level.restartStarsRoom(); break;
2059 case StartMode.Sun: level.restartSunRoom(); break;
2060 case StartMode.Moon: level.restartMoonRoom(); break;
2062 level.generateNormalLevel();
2063 if (startMode == StartMode.Dead) {
2064 level.player.dead = true;
2065 level.player.visible = false;
2071 //global.rope = 666;
2072 //global.bombs = 666;
2074 //global.globalRoomSeed = 871520037;
2075 //global.globalOtherSeed = 1047036290;
2077 //level.createTitleRoom();
2078 //level.createTrans4Room();
2079 //level.createOlmecRoom();
2080 //level.generateLevel();
2082 //level.centerViewAtPlayer();
2083 teleportCameraAt(level.viewStart);
2084 //writeln(Video.swapInterval);
2086 Video.runEventLoop();
2087 Video.closeScreen();
2088 SoundSystem.Shutdown();
2090 if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2098 // ////////////////////////////////////////////////////////////////////////// //
2099 // duplicates are not allowed!
2100 final void checkGameObjNames () {
2101 array!(class!Object) known;
2103 int classCount = 0, namedCount = 0;
2104 foreach AllClasses(Object, out cc) {
2105 auto gn = GetClassGameObjName(cc);
2107 //writeln("'", gn, "' is `", GetClassName(cc), "`");
2108 auto nid = NameToInt(gn);
2109 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));
2115 writeln(classCount, " classes, ", namedCount, " game object classes.");
2119 // ////////////////////////////////////////////////////////////////////////// //
2120 #include "timelimit.vc"
2121 //const int TimeLimitDate = 2018232;
2124 void performTimeCheck () {
2125 #ifdef DISABLE_TIME_CHECK
2127 if (TigerEye) return;
2130 if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2133 if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2135 int tldate = tm.year*1000+tm.yday;
2137 if (tldate > TimeLimitDate) {
2138 level.maxPlayingTime = 24;
2140 //writeln("*** days left: ", TimeLimitDate-tldate);
2146 void setupCheats () {
2149 global.currLevel = 2;
2150 startMode = StartMode.Alive;
2153 global.currLevel = 5;
2154 startMode = StartMode.Alive;
2155 global.scumGenLake = true;
2156 global.config.scale = 1;
2159 startMode = StartMode.Alive;
2160 global.cheatCanSkipOlmec = true;
2161 global.currLevel = 16;
2162 //global.currLevel = 5;
2163 //global.currLevel = 13;
2164 //global.config.scale = 1;
2166 //startMode = StartMode.Dead;
2167 //startMode = StartMode.Title;
2168 //startMode = StartMode.Stars;
2169 //startMode = StartMode.Sun;
2170 startMode = StartMode.Moon;
2172 //global.scumGenSacrificePit = true;
2173 //global.scumAlwaysSacrificeAltar = true;
2175 // first lush jungle level
2176 //global.levelType = 1;
2178 global.scumGenCemetary = true;
2180 //global.idol = false;
2181 //global.currLevel = 5;
2183 //global.isTunnelMan = true;
2186 //global.currLevel = 5;
2187 //global.scumGenLake = true;
2189 //global.currLevel = 5;
2190 //global.currLevel = 9;
2191 //global.currLevel = 13;
2192 //global.currLevel = 14;
2193 //global.cheatEndGameSequence = 1;
2196 //global.currLevel = 6;
2197 global.scumGenAlienCraft = true;
2198 global.currLevel = 9;
2199 //global.scumGenYetiLair = true;
2200 //global.genBlackMarket = true;
2201 //startDead = false;
2202 startMode = StartMode.Alive;
2205 global.cheatCanSkipOlmec = true;
2206 global.currLevel = 15;
2207 startMode = StartMode.Alive;
2210 global.scumGenShop = true;
2211 //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2212 global.scumGenShopType = GameGlobal::ShopType.Craps;
2213 //global.scumGenShopType = 6; // craps
2214 //global.scumGenShopType = 7; // kissing
2216 //global.scumAlwaysSacrificeAltar = true;
2220 void setupSeeds () {
2224 // ////////////////////////////////////////////////////////////////////////// //
2226 checkGameObjNames();
2228 appSetName("k8spelunky");
2229 config = SpawnObject(GameConfig);
2230 global = SpawnObject(GameGlobal);
2231 global.config = config;
2232 config.heroType = GameConfig::Hero.Spelunker;
2234 global.randomizeSeedAll();
2236 fillCheatPickupList();
2237 fillCheatItemsList();
2238 fillCheatEnemiesList();
2241 loadKeyboardBindings();