roguelike-pumpkin-patch
Version:
A roguelike development library in JavaScript.
543 lines (470 loc) • 19.6 kB
HTML
<!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 © <a href="https://laurheth.itch.io/">Lauren Hetherington</a> 2021</p>
</div>
</footer>
<script src="./scriptbundle.js"></script>
</body>
</html>