UNPKG

hobgoblin

Version:

A framework for writing roguelike games in JavaScript, implementing ROT.js

695 lines (660 loc) 28.9 kB
// Start splash screen Game.Screen.startScreen = new Game.Screen.basicScreen({ enter: function() { console.log('Entered teh start screen'); }, exit: function() { console.log('Exited the start screen'); }, render: function(display) { var w = Game.getScreenWidth(); var h = Game.getScreenHeight(); // Render prompt to the screen display.drawText((w/2) - 17, 5, "%c{yellow}[Your Game]%c{white}, a JavaScript Roguelike"); display.drawText((w/2) - 15, 6, "Press [?] at any time for help"); display.drawText((w/2) - 12, 8, "Press [Enter] to start!"); }, handleInput: function(inputType, inputData) { // When [Enter] is pressed, go to the play screen if(inputType === 'keydown' && inputData.keyCode === ROT.VK_RETURN) { Game.switchScreen(Game.Screen.loadScreen); } } }); // Displayed while files are loading and things are being generated Game.Screen.loadScreen = new Game.Screen.basicScreen({ enter: function() { // Register test modules Game.loadProgress.registerModule('Map'); Game.loadProgress.registerModule('Map', 'Lava'); Game.loadProgress.registerModule('Map', 'Water'); Game.loadProgress.registerModule('Map', 'Gold'); Game.loadProgress.registerModule('Monsters'); Game.loadProgress.registerModule('Items'); }, exit: function() {}, render: function(display) { var w = Game.getScreenWidth(); var h = Game.getScreenHeight(); var progress = Game.loadProgress.getProgress(); // 100 being the max progress number var barWidthMax = (w - 2) / 100; // -2 to account for end brackets // Due to an anomaly with l and rpad, 0 will add a pad, 1 will not (since // it gets the diff) so, if barWidth or barDiff are 0, then default to 1. var barWidth = progress * barWidthMax || 1; if(barWidth === 100 * barWidthMax) barWidth -= 1; // So as to account for the cap char var barDiff = (100 * barWidthMax) - barWidth || 1; var bar = "[".rpad("=", barWidth); var end = "]".lpad(" ", barDiff); var progressBar = bar + end; // The length of this string should always be 78 (or w - 2) // Render prompt to the screen display.drawText((w/2) - 5, 5, "%c{yellow}Loading..."); display.drawText((w/2) - (progressBar.length / 2), 7, progressBar); if(progress < 1) display.drawText((w/2) - 15, 9, "Press [Enter] to begin loading!"); if(progress >= 100) display.drawText((w/2) - 24, 9, "Press [Enter] to play or [Escape] to load again!"); }, handleInput: function(inputType, inputData) { // Purely as a demo, not functionally loading anything var numModules = 5, iterations = numModules * 10, currIteration = 1; Game.loadProgress.startModule('Map'); function loadModules() { switch(currIteration % numModules) { case 0: Game.loadProgress.startSubmodule('Map', 'Lava'); Game.loadProgress.updateSubmodule('Map', 'Lava', currIteration * 2); break; case 1: Game.loadProgress.startSubmodule('Map', 'Water'); Game.loadProgress.updateSubmodule('Map', 'Water', currIteration * 2); break; case 2: Game.loadProgress.startSubmodule('Map', 'Gold'); Game.loadProgress.updateSubmodule('Map', 'Gold', currIteration * 2); break; case 3: Game.loadProgress.startModule('Monsters'); Game.loadProgress.updateModule('Monsters', currIteration * 2); break; case 4: Game.loadProgress.startModule('Items'); Game.loadProgress.updateModule('Items', currIteration * 2); break; default: break; } if(currIteration === iterations) { Game.loadProgress.finishModule('Map'); Game.loadProgress.finishModule('Monsters'); Game.loadProgress.finishModule('Items'); clearInterval(window.intervalID); } else { currIteration++; } Game.refresh(); } if(inputType == 'keydown') { if(Game.loadProgress.getProgress() < 100) { if(inputData.keyCode === ROT.VK_RETURN) window.intervalID = setInterval(loadModules, 50); else if(inputData.keyCode === ROT.VK_ESCAPE) clearInterval(window.intervalID); } else { if(inputData.keyCode === ROT.VK_RETURN) Game.switchScreen(Game.Screen.playScreen); else if(inputData.keyCode === ROT.VK_ESCAPE) window.intervalID = setInterval(loadModules, 50); } } } }); // Main play screen Game.Screen.playScreen = new Game.Screen.basicScreen({ _player: null, _gameEnded: false, _subScreen: null, enter: function() { var width = 100; var height = 48; var depth = 6; // Create our map from the tiles and player this._player = new Game.Entity(Game.PlayerTemplate); var map = new Game.Map(width, height, depth, this._player); // Start the map's engine map.getEngine().start(); }, exit: function() { console.log("Exited play screen."); }, render: function(display) { // Render subscreen if there is one if (this._subScreen) { this._subScreen.render(display); return; } // Otherwise, procede as usual... var screenWidth = Game.getScreenWidth(); var screenHeight = Game.getScreenHeight(); // Render the tiles this.renderTiles(display); // Get the messages in the player's queue and render them var messages = this._player.getMessages(); var messageY = 0; for (var i = 0; i < messages.length; i++) { // Draw each message, adding the number of lines messageY += display.drawText( 0, messageY, '%c{white}%b{black}' + messages[i] ); } // Render player HP var stats = '%c{white}%b{black}'; stats += String.format( 'HP: %s/%s Level: %s XP: %s', this._player.getHp(), this._player.getMaxHp(), this._player.getLevel(), this._player.getExperience() ); display.drawText(0, screenHeight, stats); // Render hunger state var hungerState = this._player.getHungerState(); display.drawText(screenWidth - hungerState.length, screenHeight, hungerState); }, move: function(dX, dY, dZ) { var newX = this._player.getX() + dX; var newY = this._player.getY() + dY; var newZ = this._player.getZ() + dZ; this._player.tryMove(newX, newY, newZ, this._player.getMap()); }, handleInput: function(inputType, inputData) { // If the game is over, enter will bring the user to the losing screen. if(this._gameEnded) { if (inputType === 'keydown' && inputData.keyCode === ROT.VK_RETURN) { Game.switchScreen(Game.Screen.loseScreen); } // Return to make sure the user can't still play return; } // Handle subscreen input if there is one if (this._subScreen) { this._subScreen.handleInput(inputType, inputData); return; } var command = Game.Input.handleInput('playScreen', inputType, inputData); var unlock = command ? command(this._player) : false; if(unlock) this._player.getMap().getEngine().unlock(); else Game.refresh(); }, getScreenOffsets: function() { // Make sure we still have enough space to fit an entire game screen var topLeftX = Math.max(0, this._player.getX() - (Game.getScreenWidth() / 2)); // Make sure we still have enough space to fit an entire game screen topLeftX = Math.min(topLeftX, this._player.getMap().getWidth() - Game.getScreenWidth()); // Make sure the y-axis doesn't above the top bound var topLeftY = Math.max(0, this._player.getY() - (Game.getScreenHeight() / 2)); // Make sure we still have enough space to fit an entire game screen topLeftY = Math.min(topLeftY, this._player.getMap().getHeight() - Game.getScreenHeight()); return { x: topLeftX, y: topLeftY }; }, renderTiles: function(display) { var screenWidth = Game.getScreenWidth(); var screenHeight = Game.getScreenHeight(); var offsets = this.getScreenOffsets(); var topLeftX = offsets.x; var topLeftY = offsets.y; // This object will keep track of all visible map cells var visibleCells = {}; // Store this._player.getMap() and player's z to prevent losing it in callbacks var map = this._player.getMap(); var currentDepth = this._player.getZ(); // Find all visible cells and update the object map.getFov(currentDepth).compute( this._player.getX(), this._player.getY(), this._player.getSightRadius(), function(x, y, radius, visibility) { visibleCells[x + "," + y] = true; // Mark cell as explored map.setExplored(x, y, currentDepth, true); }); // Iterate through visible map cells for (var x = topLeftX; x < topLeftX + screenWidth; x++) { for (var y = topLeftY; y < topLeftY + screenHeight; y++) { if (map.isExplored(x, y, currentDepth)) { // Fetch the glyph for the tile and render it to the screen // at the offset position. var glyph = map.getTile(x, y, currentDepth); var foreground = glyph.getForeground(); // If we are at a cell that is in the field of vision, we need // to check if there are items or entities. if (visibleCells[x + ',' + y]) { // Check for items first, since we want to draw entities // over items. var items = map.getItemsAt(x, y, currentDepth); // If we have items, we want to render the top most item if (items) { glyph = items[items.length - 1]; } // Check if we have an entity at the position if (map.getEntityAt(x, y, currentDepth)) { glyph = map.getEntityAt(x, y, currentDepth); } // Update the foreground color in case our glyph changed foreground = glyph.getForeground(); } else { // Since the tile was previously explored but is not // visible, we want to change the foreground color to // dark gray. foreground = 'darkGray'; } display.draw( x - topLeftX, y - topLeftY, glyph.getChar(), foreground, glyph.getBackground()); } } } // Render the entities var entities = this._player.getMap().getEntities(); for (var key in entities) { var entity = entities[key]; if (visibleCells[entity.getX() + ',' + entity.getY()]) { // Only render the entity if they would show up on the screen if(entity.getX() < topLeftX + screenWidth && entity.getX() >= topLeftX && entity.getY() < topLeftY + screenHeight && entity.getY() >= topLeftY && entity.getZ() == this._player.getZ()) { display.draw( entity.getX() - topLeftX, entity.getY() - topLeftY, entity.getChar(), entity.getForeground(), entity.getBackground() ); } } } }, setGameEnded: function(gameEnded) { this._gameEnded = gameEnded; }, getSubScreen: function() { return this._subScreen; }, setSubScreen: function(subScreen) { this._subScreen = subScreen; Game.refresh(); }, showItemsSubScreen: function(subScreen, items, emptyMessage) { if (items && subScreen.setup(this._player, items) > 0) { this.setSubScreen(subScreen); } else { Game.sendMessage(this._player, emptyMessage); Game.refresh(); } } }); // Inventory sub-screens Game.Screen.inventoryScreen = new Game.Screen.ItemListScreen({ caption: 'Inventory', canSelect: false }); Game.Screen.pickupScreen = new Game.Screen.ItemListScreen({ caption: 'Choose the items you wish to pickup', canSelect: true, canSelectMultipleItems: true, ok: function(selectedItems) { // Try to pick up all items, messaging the player if they couldn't all be picked up. if (!this._player.pickupItems(Object.keys(selectedItems))) { Game.sendMessage(this._player, "Your inventory is full! Not all items were picked up."); } return true; } }); Game.Screen.dropScreen = new Game.Screen.ItemListScreen({ caption: 'Choose the item you wish to drop', canSelect: true, canSelectMultipleItems: false, ok: function(selectedItems) { // Drop the selected item this._player.dropItem(Object.keys(selectedItems)[0]); return true; } }); Game.Screen.eatScreen = new Game.Screen.ItemListScreen({ caption: 'Choose the item you wish to eat', canSelect: true, canSelectMultipleItems: false, isAcceptable: function(item) { return item && item.hasMixin('Edible') && item !== this._player._armor && item !== this._player._weapon; }, ok: function(selectedItems) { // Eat the item, removing it if there are no consumptions remaining. var key = Object.keys(selectedItems)[0]; var item = selectedItems[key]; Game.sendMessage(this._player, "You eat %s.", [item.describeThe()]); item.eat(this._player); if (!item.hasRemainingConsumptions()) { this._player.removeItem(key); } return true; } }); Game.Screen.wieldScreen = new Game.Screen.ItemListScreen({ caption: 'Choose the item you wish to wield', canSelect: true, canSelectMultipleItems: false, hasNoItemOption: true, isAcceptable: function(item) { return item && item.hasMixin('Equippable') && item.isWieldable(); }, ok: function(selectedItems) { // Check if we selected 'no item' var keys = Object.keys(selectedItems); if (keys.length === 0) { this._player.unwield(); Game.sendMessage(this._player, "You are empty handed.") } else { // Make sure to unequip the item first in case it is the armor. var item = selectedItems[keys[0]]; this._player.unequip(item); this._player.wield(item); Game.sendMessage(this._player, "You are wielding %s.", [item.describeA()]); } return true; } }); Game.Screen.wearScreen = new Game.Screen.ItemListScreen({ caption: 'Choose the item you wish to wear', canSelect: true, canSelectMultipleItems: false, hasNoItemOption: true, isAcceptable: function(item) { return item && item.hasMixin('Equippable') && item.isWearable(); }, ok: function(selectedItems) { // Check if we selected 'no item' var keys = Object.keys(selectedItems); if (keys.length === 0) { this._player.unwield(); Game.sendMessage(this._player, "You are not wearing anthing.") } else { // Make sure to unequip the item first in case it is the weapon. var item = selectedItems[keys[0]]; this._player.unequip(item); this._player.wear(item); Game.sendMessage(this._player, "You are wearing %s.", [item.describeA()]); } return true; } }); Game.Screen.examineScreen = new Game.Screen.ItemListScreen({ caption: 'Choose the item you wish to examine', canSelect: true, canSelectMultipleItems: false, isAcceptable: function(item) { return true; }, ok: function(selectedItems) { var keys = Object.keys(selectedItems); if (keys.length > 0) { var item = selectedItems[keys[0]]; var description = "It's %s"; var details = item.details(); if(details && details != "") { description += " (%s)."; Game.sendMessage(this._player, description, [ item.describeA(false), item.details() ]); } else { Game.sendMessage(this._player, description, [item.describeA(false)]); } } return true; } }); Game.Screen.throwScreen = new Game.Screen.ItemListScreen({ caption: 'Choose the item you wish to throw', canSelect: true, canSelectMultipleItems: false, isAcceptable: function(item) { if(!item || !item.hasMixin('Throwable')) { return false; } else if(item.hasMixin('Equippable') && (item.isWielded() || item.isWorn())) { return false } else { return true; } }, ok: function(selectedItems) { var offsets = Game.Screen.playScreen.getScreenOffsets(); // Go to the targetting screen Game.Screen.throwTargetScreen.setup(this._player, this._player.getX(), this._player.getY(), offsets.x, offsets.y); this._player.setThrowing(Object.keys(selectedItems)[0]); Game.Screen.playScreen.setSubScreen(Game.Screen.throwTargetScreen); return; } }); // Target-based screens Game.Screen.lookScreen = new Game.Screen.TargetBasedScreen({ captionFunction: function(x, y) { var z = this._player.getZ(); var map = this._player.getMap(); // If the tile is explored, we can give a better caption if (map.isExplored(x, y, z)) { // If the tile isn't explored, we have to check if we can actually // see it before testing if there's an entity or item. if (this._visibleCells[x + ',' + y]) { var items = map.getItemsAt(x, y, z); // If we have items, we want to render the top most item if (items) { var item = items[items.length - 1]; return String.format('%s - %s (%s)', item.getRepresentation(), item.describeA(true), item.details()); // Else check if there's an entity } else if (map.getEntityAt(x, y, z)) { var entity = map.getEntityAt(x, y, z); return String.format('%s - %s (%s)', entity.getRepresentation(), entity.describeA(true), entity.details()); } } // If there was no entity/item or the tile wasn't visible, then use // the tile information. return String.format('%s - %s', map.getTile(x, y, z).getRepresentation(), map.getTile(x, y, z).getDescription()); } else { var nullTile = Game.TileRepository.create('null'); // If the tile is not explored, show the null tile description. return String.format('%s - %s', nullTile.getRepresentation(), nullTile.getDescription()); } } }); Game.Screen.throwTargetScreen = new Game.Screen.TargetBasedScreen({ captionFunction: function(x, y, points) { var throwing = this._player.getItems()[this._player.getThrowing()]; var throwingSkill = this._player.getThrowingSkill(); var entity = this._player.getMap().getEntityAt(x, y, this._player.getZ()); console.log(entity); var string = String.format("You are throwing %s", throwing.describeA()); if(entity) { string += String.format(" at %s", entity.describeA()); } if(points.length > throwingSkill) { string += " - Might not do as much damage at this range" } return string; }, okFunction: function(x, y) { this._player.throwItem(this._player.getThrowing(), x, y); return true; } }); // Menu-based screens Game.Screen.actionMenu = new Game.Screen.MenuScreen({ caption: 'Action Menu', buildMenuItems: function() { var adjacentCoords = Game.Geometry.getCircle(this._player.getX(), this._player.getY(), 1), map = this._player.getMap(), z = this._player.getZ(), actions = []; // Populate a list of actions with which to build the menu for(var i = 0; i < adjacentCoords.length; i++) { var coords = adjacentCoords[i].split(","), x = coords[0], y = coords[1]; var entity = map.getEntityAt(x, y, z), items = map.getItemsAt(x, y, z); if(entity) { var entityActions = entity.raiseEvent('action', this._player); if(entityActions) actions.push(entityActions); } if(items) { for(var j = 0; j < items.length; j++) { var itemActions = items[j].raiseEvent('action', this._player); if(itemActions) actions.push(itemActions); } } } // Iterate through the actions, building out the _menuItems and _menuActions arrays for(var k = 0; k < actions.length; k++) { var glyphActions = actions[k]; // An array of action objects for (var l = 0; l < glyphActions.length; l++) { // An object of action name/functions pairs returned by each relevant item-mixin listener var mixinActions = glyphActions[l]; for(var actionName in mixinActions) { this._menuItems.push(actionName); this._menuActions.push(mixinActions[actionName]); } } } } }); // Help screen Game.Screen.helpScreen = new Game.Screen.basicScreen({ enter: function(display) {}, exit: function(display) {}, render: function(display) { var text = '[Your Roguelike] Help'; var border = '---------------'; var y = 0; display.drawText(Game.getScreenWidth() / 2 - text.length / 2, y++, text); display.drawText(Game.getScreenWidth() / 2 - text.length / 2, y++, border); y += 3; display.drawText(0, y++, 'Arrow keys to move'); display.drawText(0, y++, '[<] to go up stairs'); display.drawText(0, y++, '[>] to go down stairs'); display.drawText(0, y++, '[,] to pick up items'); display.drawText(0, y++, '[d] to drop items'); display.drawText(0, y++, '[e] to eat items'); display.drawText(0, y++, '[w] to wield items'); display.drawText(0, y++, '[W] to wear items'); display.drawText(0, y++, '[t] to throw items'); display.drawText(0, y++, '[x] to examine items'); display.drawText(0, y++, '[;] to look around you'); display.drawText(0, y++, '[?] to show this help screen'); y += 3; text = '--- press any key to continue ---'; display.drawText(Game.getScreenWidth() / 2 - text.length / 2, y++, text); }, handleInput: function(inputType, inputData) { Game.Screen.playScreen.setSubScreen(null); } }); // Level-up screen Game.Screen.gainStatScreen = new Game.Screen.basicScreen({ enter: function(entity) { // Must be called before rendering. this._entity = entity; this._options = entity.getStatOptions(); Game.Screen.playScreen.setSubScreen(Game.Screen.gainStatScreen); }, exit: function() { Game.Screen.playScreen.setSubScreen(undefined); }, render: function(display) { var letters = 'abcdefghijklmnopqrstuvwxyz'; display.drawText(0, 0, 'Choose a stat to increase: '); // Iterate through each of our options for (var i = 0; i < this._options.length; i++) { display.drawText(0, 2 + i, letters.substring(i, i + 1) + ' - ' + this._options[i][0]); } // Render remaining stat points display.drawText(0, 4 + this._options.length, "Remaining points: " + this._entity.getStatPoints()); }, handleInput: function(inputType, inputData) { if (inputType === 'keydown') { // If a letter was pressed, check if it matches to a valid option. if (inputData.keyCode >= ROT.VK_A && inputData.keyCode <= ROT.VK_Z) { // Check if it maps to a valid item by subtracting 'a' from the character // to know what letter of the alphabet we used. var index = inputData.keyCode - ROT.VK_A; if (this._options[index]) { // Call the stat increasing function this._options[index][1].call(this._entity); // Decrease stat points this._entity.setStatPoints(this._entity.getStatPoints() - 1); // If we have no stat points left, exit the screen, else refresh if (this._entity.getStatPoints() === 0) { this.exit(); } else { Game.refresh(); } } } } } }); // Define our winning screen Game.Screen.winScreen = new Game.Screen.basicScreen({ enter: function() { console.log("Entered win screen."); }, exit: function() { console.log("Exited win screen."); }, render: function(display) { // Render our prompt to the screen for (var i = 0; i < 22; i++) { // Generate random background colors var r = Math.round(Math.random() * 255); var g = Math.round(Math.random() * 255); var b = Math.round(Math.random() * 255); var background = ROT.Color.toRGB([r, g, b]); display.drawText(2, i + 1, "%b{" + background + "}You win!"); } }, handleInput: function(inputType, inputData) { if(inputType === 'keydown' && inputData.keyCode === ROT.VK_RETURN) { Game.switchScreen(Game.Screen.playScreen); } } }); // Define our winning screen Game.Screen.loseScreen = new Game.Screen.basicScreen({ enter: function() { console.log("Entered lose screen."); }, exit: function() { console.log("Exited lose screen."); }, render: function(display) { // Render our prompt to the screen for (var i = 0; i < 22; i++) { display.drawText(2, i + 1, "%b{red}You lose! :("); } }, handleInput: function(inputType, inputData) { if(inputType === 'keydown' && inputData.keyCode === ROT.VK_RETURN) { Game.Screen.playScreen.setGameEnded(true); Game.switchScreen(Game.Screen.startScreen); } } });