UNPKG

roguelike-pumpkin-patch

Version:
543 lines (470 loc) 19.6 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Roguelike Pumpkin Patch Documentation</title> <!-- Favicon stuff --> <link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png"> <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="./favicon-16x16.png"> <link rel="manifest" href="./site.webmanifest"> <!-- Syntax Highlighting --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/styles/railscasts.min.css"> <!-- Fonts --> <link rel="preconnect" href="https://fonts.gstatic.com"> <link href="https://fonts.googleapis.com/css2?family=Cousine:wght@700&family=Zilla+Slab&display=swap" rel="stylesheet"> <!-- Stylesheet --> <link rel="stylesheet" href="./style.css"> <!-- Do the actual syntax highlighting --> <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.5.0/highlight.min.js"></script> <script>hljs.initHighlightingOnLoad();</script> </head> <body> <div class="wrapper"> <header> <h1>Roguelike Pumpkin Patch Documentation</h1> <p>Roguelike Pumpkin Patch is a library for building roguelikes that run in your browser, written in TypeScript or JavaScript. This documentation is meant to show you how to actually use the library. Check out the <a href="https://github.com/laurheth/roguelike-pumpkin-patch">readme here</a> for setup instructions!</p> <nav> <ul><a href="#display">Display</a></ul> <ul><a href="#random">Random numbers</a></ul> <ul><a href="#fov">Field of View</a></ul> <ul><a href="#events">The Event System</a></ul> <ul><a href="#pathfinder">Pathfinding</a></ul> </nav> </header> </div> <div class="wrapper"> <main> <section class="display" id="display"> <h2>The Display</h2> <p> The display is where most of the magic happens. Contains a multitude of tiles, and is based on HTML and CSS. Suppose you have the following as your CSS: </p> <pre><code> /* You can style the tiles with pumpkin-tile! You can also target pumpkin-display and pumpkin-container */ .pumpkin-tile { font-family: monospace; } /* The display will fit the container you put it in; make sure it has a size! */ .pumpkin-container { height: 400px; } /* Just some styles that you think look cool! */ .brickWall { background: darkred; color: gray; } .player { color: white; } .coolFloor { color: green; } .superAwesome { background: purple; color: white; font-family: sans-serif; } </code></pre> <p> Usage of the display might look like: </p> <pre><code> // First, select the target element you want the display to be within const target = document.getElementById("displayExample"); // Paramaters object const params = { // Required! The display must go somewhere target: target, // Width of the display in tiles width: 20, // Height of the display in tiles height: 15, }; // Start the display! const display = new Display(params); // Set the tile size so that it fits its container display.tileSize = display.calculateTileSize(); // One cool thing you can do is add a listener for window resizing // Keep your display looking good! window.addEventListener("resize",()=>{ display.tileSize = display.calculateTileSize(); }); // Lets draw some stuff // Note, x and y are relative to the top left corner! for (let x=0; x < params.width; x++) { for (let y=0; y < params.height; y++) { // Draw some walls if (x===0 || y===0 || x===params.width-1 || y===params.height-1) { display.setTile(x,y,{ // Content can be a string, or ANY html element! (including images!) content: '#', className: "brickWall" }); // Add our player! } else if (x===3 && y===5) { display.setTile(x,y,{ content: '@', // You can use as many classes as you would like! classList: ["player"] }); // Some floor everywhere else } else { display.setTile(x,y,{ content: '.', className: "coolFloor" }); } } } // Hmm actually I want to change some of the tiles a bit. updateTile changes the // parameters that you specify; setTile replaces everything. for (let i=5; i < 10; i++) { display.updateTile(i,i,{className:"superAwesome"}); } </code></pre> <p> The results will be: </p> <div id="displayExample"></div> <p> There are some other methods you might enjoy. If you want to center the display on a particular location (i.e. the player's position, or a security camera, or a cutscene, etc), you can use centerDisplay: </p> <pre><code> display.centerDisplay(x,y); </code></pre> <p> If you want to automatically determine how many tiles you need to fill the available space, given whatever size they currently are, you can use calculateDimensions: </p> </p> <pre><code> display.dimensions = display.calculateDimensions(); </code></pre> <p> If you want to do something more in depth with a particular tile, you can use getTile to access it: </p> <pre><code> const tile = display.getTile(x,y); </code></pre> <p> Lastly, while the default style for the display is a black background with white foreground, this is handled by CSS inserted into the pages head; you can just override it, and set any other styles you'd like, with your own stylesheet by accessing pumpkin-container. The only details that are handled inline are the font size, tile size, and tile position, and you can use the tileSize setter to change the first two. </p> <pre><code> .pumpkin-container.colorDisplay { background: linear-gradient(#FF00FF, #FFFF00); color: darkslateblue; font-weight: bold; } </code></pre> <pre><code> // Select the element we want to be the target. const colorTarget = document.querySelector(".colorDisplay"); // Initialize the colorful display const colorDisplay = new Display({target:colorTarget, width: 10, height: 10}); // Draw some stuff for(let i=0;i < 10;i++) { for(let j=0;j < 10;j++) { if (i===0 || j===0 || i===9 || j===9) { // Did you know you can use a shorthand here? Now you do! colorDisplay.setTile(i,j,'#'); } else if (i===3 && j===3) { colorDisplay.setTile(i,j,'@'); } else if (i===4 && j===5) { colorDisplay.setTile(i,j,{ content: 'g', // You can use inline colors and backgrounds if // you REALLY want to, but I discourage it. color: 'green', background: 'rgba(128,0,0,0.2)', }); } else { colorDisplay.setTile(i,j,'.'); } } } // Make the display fit the container colorDisplay.tileSize = colorDisplay.calculateTileSize(); // Center it on the player colorDisplay.centerDisplay(3,3); </code></pre> <div id="colorfulDisplay" class="colorDisplay"></div> <p>Psychadelic!</p> </section> <section class="random" id="random"> <h2>Random Numbers, and related utilities</h2> <p>Generating random numbers is fun for the whole family. Here's how you do it.</p> <pre> <code> // You can define a seed if you would like! If not, the generator figures out its own. const optionalSeed = Date.now(); // Start the random number generator const random = new Random(optionalSeed); // If you want a random number from 0 <= x < 1: const x = random.getRandom(); // If you would like a random number in the range of lower <= y <= upper you can use getNumber. const lower = 0; const upper = 10; const y = random.getNumber(lower,upper); // If the given bounds are integers, it will generate integers. // If not, it will generate decimals. // If you want to specify explicitly, use the integer boolean parameter. const noThanksNotInteger = random.getNumber(lower,upper,false); // If you want a random element from an array, use getRandomElement. const coolArray = [1, 2, 3, 4, 5, 6, 7]; const randomElement = random.getRandomElement(coolArray); // If you want to provide weights for each value, you can use getWeightedElement. const weightedArray = [ { weight: 10, option: "Cute dog" }, { weight: 15, option: "Awesome cat" }, { weight: 1, option: "Rare Franklin" } ]; const randomWeightedElement = random.getWeightedElement(weightedArray); </code></pre> <p> Here's some results: </p> <ul id="randomResults"> </ul> <button id="randomButton">Get me more random stuff!</button> <p> The random number generator uses an implementation of the middle square Weyl sequence RNG, described in <a href="https://arxiv.org/abs/1704.00358v5">Widynski (2017)</a>. </p> </section> <section class="fov" id="fov"> <h2>Field of View</h2> <p>It's nice to see things. Roguelike Pumpkin Patch lets you do it.</p> <pre><code> // Here's a cool map to live in const map = [ "####################", "#..................#", "#..#.....#....#....#", "#..#.....###...##..#", "#..#.....#.........#", "#.............####.#", "#........#....#....#", "####...###....#....#", "#........#....#....#", "####################", ]; const width = map[0].length; const height = map.length; // And lets start up a display to use const fovDisplayParams = { target: document.getElementById("fovMap"), width: width, height: height, }; const fovDisplay = new Display(fovDisplayParams); // FOV takes a callback function that decides whether or not you can see something. // It takes one parameter, which is a two element position array pos = [x,y] // and returns true or false. const canSee = (position) => { const x = position[0]; const y = position[1]; // Make sure it's even on the map if ( x<0 || x>=width || y<0 || y>=height) { return false; } const tile = map[y][x]; // First, regardless of success or not, see this tile fovDisplay.setTile(x,y,tile); // Next, use whatever criteria we want to decide if it is seethrough or not. // In this case, if it's not a wall (or # character), we can see through it. return tile !== '#'; } // It has an optional second parameter for distance. The default is 8. // Be careful not to set this too high though. const optionalRange = 20; // Initialize the FOV object! const fov = new FOV(canSee, optionalRange); // Choose a position for the player to be const playerPos = [5,5]; // Slightly hacky way to add the player; use a better data structure for your games! const mapRow = map[playerPos[1]]; map[playerPos[1]] = mapRow.slice(0,playerPos[0]) + '@' + mapRow.slice(playerPos[0]); // Now, LOOK! fov.look(playerPos); </code></pre> <div id="fovMap"></div> <p> FOV currently uses a shadow casting algorithm. It works well in most cases, but be careful not to set the range too high, especially if your game has large outdoor areas. </p> </section> <section class="events" id="events"> <h2>The Events System</h2> <p>Events are cool. Actions are cool. How do you make them happen? Why, with the EventManager, of course!</p> <pre><code> // First, some setup, so we can record our output const simpleList = document.getElementById("simpleEventList"); const complexList = document.getElementById("complexEventList"); // Lets make a helper function to record our output const showAction = (action, list) => { // Make a list item and add the action to it. const listItem = document.createElement("li") listItem.appendChild(document.createTextNode(action)); // Attach it to the list, so we can see what happens. list.appendChild(listItem); }; // There's two types of event managers you can make. // Lets start with the simple one. Everyone takes turns, one after the other. const simpleEvents = new EventManager({type:"simple"}); // Usually, we want to add some actors to the system. // The system calls their "act" method. const simpleGoblin = { act: ()=>showAction("The Goblin goblins!", simpleList) } const simpleCat = { act: ()=>showAction("The cat meows!", simpleList) } // Add them to the event manager simpleEvents.add(simpleGoblin); simpleEvents.add(simpleCat); // You can also add callback functions on their own, as events or whatever your heart pleases. // They can repeat forever... simpleEvents.add({ callback: ()=>showAction("Drip drip goes the faucet.", simpleList), repeats: true }) // Repeat a few times simpleEvents.add({ callback: ()=>showAction("Rushing wind! Oh no!", simpleList), repeats: 2 }) // Or not repeat at all! simpleEvents.add({ callback: ()=>showAction("The house of cards falls over. Whoops!", simpleList) }); // Then you just kick it off in your preferred manner. // Each time you call advance, it will step forward one step. // If act returns a promise (say, if you're waiting for player input) // it will wait for that action to conclude. for(let i=0;i<20;i++) { simpleEvents.advance(); } // The second type of event manager is complex: const complexEvents = new EventManager({type:"complex"}); // The complex event manager accepts different delays for different actors const fastCat = { act:()=>showAction("Fast cat nyooms!", complexList) } const slowOgre = { act:()=>showAction("Slow ogre is sloooow", complexList) } // The delay property defines how slow an actor is complexEvents.add({ actor:fastCat, delay:1 }); complexEvents.add({ actor:slowOgre, delay:5 }); // Or how long an event takes complexEvents.add({ callback:()=>showAction("The mail has just arrived. Sweet!", complexList), delay: 16 }); // Advance the clock... for(let i=0;i < 20;i++) { complexEvents.advance(); } </code></pre> <div> <h3>Simple Event System:</h3> <ul id="simpleEventList"></ul> <h3>Complex Event System:</h3> <ul id="complexEventList"></ul> </div> </section> <section class="pathfinder" id="pathfinder"> <h2>Pathfinding</h2> <p>Finding your way from one place to another can be tough. We can help!</p> <pre><code> // First, lets setup another display! I want to draw the path we find. // And lets start up a display to use. // We're going to use the same map from the FOV section. const pathDisplayParams = { target: document.getElementById("pathDisplay"), width: width, height: height, }; const pathDisplay = new Display(pathDisplayParams); pathDisplay.tileSize = pathDisplay.calculateTileSize(); // Lets draw the map to start map.forEach((row,y)=>row.split('').forEach((tile,x)=>{ pathDisplay.setTile(x,y,tile); })); // Now, lets setup the pathfinder! // The PathFinder takes a "canPass" callback to determine what is passable. // This looks similar to the canSee callback from before, but it doesn't have to. const pathfinder = new PathFinder({ canPass:([x,y])=>{ // Make sure it's even on the map if ( x<0 || x>=width || y<0 || y>=height) { return false; } const tile = map[y][x]; // Next, use whatever criteria we want to decide if it is passable. // In this case, if it's not a wall (or # character), we can walk through it. return tile !== '#'; } }); // Let choose a starting position, and a target position! const startPos = [1,8]; const endPos = [15,8]; // It can also take an optional "orthogonalOnly" parameter. // This sets whether or not the pathfinder will use diagonals. const optionalOrthogonalOnly = false; // Now, lets find the path! const path = pathfinder.findPath(startPos, endPos, optionalOrthogonalOnly); // Draw it onto the map to take a look at it. path.forEach(([x,y])=>{ pathDisplay.updateTile(x,y,{ content:'X', className: "pathMarker" }) }); // Note that the drawn path doesn't include the starting position. // This is so you can just grab path[0] to get the first step in your journey. // Lets draw on the player, too, for illustration. pathDisplay.updateTile(startPos[0], startPos[1], '@'); </code></pre> <div id="pathDisplay"></div> <p> Other cool parameters that you can give the PathFinder include: </p> <ul> <li> a weight callback function, to set if certain tiles cost more to walk through (i.e. mud, rocks, mud, monsters...) </li> <li> A custom metric callback function. Currently, the PathFinder uses the A* pathfinding algorithm with the Manhattan metric; you can provide your own metric instead! </li> <li> A maxIterations number. If you want to prevent monsters from calculating extremely large paths, you can prevent that by placing a cap on it. </li> </ul> </section> </main> </div> <footer> <div class="wrapper"> <p>Syntax highlighting in code blocks from <a href="https://highlightjs.org/">highlight.js</a>; it's pretty cool, you should try it.</p> <p>Roguelike Pumpkin Patch is &copy; <a href="https://laurheth.itch.io/">Lauren Hetherington</a> 2021</p> </div> </footer> <script src="./scriptbundle.js"></script> </body> </html>