Quick, dirty and ugly JavaScript tutorial for random generators (Part 5)
Quick, dirty and ugly JavaScript tutorial for random generators
Part 5
Welcome to the fifth part of my tutorial, you can find the first parte here, second part, third part here and the 4th one here. Now we are going to repeat some of the initial advice of that part.
There are a lot of useful tools like Perchance and Chartopia available to create random generatos for your rpg but here we are going to make things old-school, tinkering with HTML and JavaScript.
You will need a basic knowledge of how a webpage is created using HTML and some minor programming experience in any programming language so you can grasp the JavaScript parts. The code and web page will be just as simple as possible and we'll try to avoid any advanced features (or even good modern practices).
Again we are going to do something new! We'll try to improve our random map.
The story so far
There are several dungeon generatos that create beautiful maps like the incredible One Page Dungeon by Watabou or the ones in the donjon web.
The idea was to generate a SVG image with our code and then draw rooms on it. A similar approach could be done using the HTML5 Canvas, but we will not cover it here.
On the last part we had a small string of randomly placed rooms, simple but effective, but now we want to add some eyecandy to it.
Clean up, Big map and Doors
To be able to see things well make our map bigger so intead of 16 pixels per tile we'll upsize it to 50. And make our map also have more tiles too, lets say 40x30. So at the start of out script will change the definition of the varible cs (used to define the cell size) and add a couple of extra lines too. This time we'll define properly as conts as their value will not change.
const cs = 25;
const maxX = 40;
const maxY = 30;
var yTile = Math.floor(maxY/2);
while (xTile < maxX - 6) {
to make things easier I'll just post here to complete revamped code. The functionality
<!DOCTYPE html>
<html>
<head>
<title>My RPG Map Generator</title>
<script>
const cs = 25;
const maxX = 40;
const maxY = 30;
function CreateRoom (x,y, w, h) {
var RoomTxt = '<rect x="'+x+'" y="'+y+'" width="'+(w*cs)+'" height="'+(h*cs)+'" style="fill:white;stroke:black;stroke-width:2;" />';
RoomTxt = RoomTxt + '<g stroke="black" stroke-width="1">';
for (var i = 1; i < w; i++ ) {
RoomTxt = RoomTxt + '<path stroke-dasharray="3,3" d="M'+(x+i*cs)+' '+y+' '+(x+i*cs)+' '+(y+h*cs)+'" />';
};
for (var i = 1; i < h; i++ ) {
RoomTxt = RoomTxt + '<path stroke-dasharray="3,3" d="M'+(x)+' '+(y+i*cs)+' '+(x+w*cs)+' '+(y+i*cs)+'" />';
};
RoomTxt = RoomTxt + '</g>'+
'<g fill="none" stroke="darkgray" stroke-width="2">'+
'<path d="M'+(x+4)+' '+(y+4)+' '+(x+10)+' '+(y+4)+'" />'+
'<path d="M'+(x+4)+' '+(y+4)+' '+(x+4)+' '+(y+10)+'" />'+
'<path d="M'+(x+w*cs-4)+' '+(y+4)+' '+(x+w*cs-10)+' '+(y+4)+'" />'+
'<path d="M'+(x+w*cs-4)+' '+(y+4)+' '+(x+w*cs-4)+' '+(y+10)+'" />'+
'<path d="M'+(x+4)+' '+(y+h*cs-4)+' '+(x+10)+' '+(y+h*cs-4)+'" />'+
'<path d="M'+(x+4)+' '+(y+h*cs-4)+' '+(x+4)+' '+(y+h*cs-10)+'" />'+
'<path d="M'+(x+w*cs-4)+' '+(y+h*cs-4)+' '+(x+w*cs-10)+' '+(y+h*cs-4)+'" />'+
'<path d="M'+(x+w*cs-4)+' '+(y+h*cs-4)+' '+(x+w*cs-4)+' '+(y+h*cs-10)+'" />'+
'</g>';
return RoomTxt;
};
function createDoor(x, y) {
const doorWidth = cs;
const doorHeight = 4;
return '<rect x="${x-2}" y="${y + 3 }" width="${doorHeight}" height="${cs-6}" style="fill:white;stroke:black;stroke-width:2;" />';
}
function getDungeonMap() {
var xTile = 1;
var yTile = Math.floor(maxY/2);
var dungeon = "";
var xW;
var yH;
function makeRandomCorridor(xTile, yTile) {
let xW, yH, roomData;
xW = Math.floor(2 + Math.random() * 4);
yH = 1;
dungeon += CreateRoom(xTile * cs, yTile * cs, xW, yH);
dungeon += createDoor(xTile * cs, yTile * cs);
return { xTile: xTile + xW, yTile };
}
function makeRandomRoom(xTile, yTile, direction) {
let xW, yH, xTileN, yTileN, roomData;
xW = Math.floor(3 + Math.random() * 4);
yH = Math.floor(3 + Math.random() * 4);
yTileN = Math.min(Math.max(yTile - yH + 1 + Math.floor(Math.random() * yH), 1), maxY - yH);
dungeon += CreateRoom(xTile * cs, yTileN * cs, xW, yH);
return { xTile: xTile, yTile: yTileN, xW: xW, yH: yH };
}
while (xTile < maxX - 5) {
let branchPos;
let newPos = makeRandomCorridor(xTile, yTile);
xTile = newPos.xTile;
yTile = newPos.yTile;
newPos = makeRandomRoom(xTile, yTile);
dungeon += createDoor(xTile * cs, yTile * cs);
xTile += newPos.xW;
yTile = newPos.yTile + Math.floor(Math.random() * newPos.yH);
yTile = Math.min(maxY-2, Math.max(2, yTile));
}
document.getElementById("Random").innerHTML = '<svg width="' + ((maxX + 6) * cs) + '" height="' + (maxY * cs) + '">' + dungeon + '</svg>';
}
</script>
</head>
<body>
<h1>Ultimate Dungeon Map Generator</h1>
<button type="button" onclick="getDungeonMap ()" >Run!</button>
<p id="Random">
<svg width="1300" height="750"><rect x="5" y="5" width="1290" height="740" style="stroke-width:1;fill:white;stroke:rgb(0,0,0)"
/></svg>
</p>
</body>
</html>
Ultimate Dungeon Map Generator
That Old School Crosshatched Walls
Next step will be to add that old style crosshatching pattern around our walls. For this, we will create a function to generate the crosshatch pattern for each room and corridor. This function generates a series of random lines in a pattern, with a specified number of parallel lines, and combines them into an SVG path. These lines are then added to the SVG path data, which will be rendered on the final dungeon map. By using this function, we can easily add a unique crosshatching style to our dungeon generator, giving it a more interesting and appealing visual appearance.
function generateCrosshatchPattern() {
let patternContent = '';
const lineCount = 400;
const parallelDistance = 3;
for (let i = 0; i < lineCount; i++) {
const x1 = Math.random() * 100;
const y1 = Math.random() * 100;
const length = 16 + Math.random() * 4;
const angle = Math.random() * 360;
const x2 = x1 + length * Math.cos(angle);
const y2 = y1 + length * Math.sin(angle);
const numParallel = 3 + Math.floor(Math.random() * 2);
for (let j = 0; j < numParallel; j++) {
const xOffset = j * parallelDistance * Math.cos(angle + Math.PI / 2);
const yOffset = j * parallelDistance * Math.sin(angle + Math.PI / 2);
patternContent += `<path d="M ${x1 + xOffset} ${y1 + yOffset} L ${x2 + xOffset} ${y2 + yOffset}" stroke="white" stroke-width="4" />`;
patternContent += `<path d="M ${x1 + xOffset} ${y1 + yOffset} L ${x2 + xOffset} ${y2 + yOffset}" stroke="black" stroke-width="1" />`;
}
}
return patternContent;
}
The result will generate sets of paralel lines emulating the wall map crosshatching used dungeon maps.
So we'll add and additional rectangle to each room or corridor, larger than them, with the pattern. And to make things pop more also some shadow over it. As we want to avoid overlaping with our rooms properly we should create a new pair of string variables to store the instructions to paint this!
First we initialize both variables at the start of our GetDungeonMap function, just after the dungeon string.
var dungeon = "";
var shadow = '<path d="';
var crosshatch = '<rect x="5" y="5" width="' + ((maxX + 6) * cs - 10) + '" height="' + (maxY * cs -10) + '" style="stroke-width:1;fill:white;stroke:rgb(0,0,0)" />';
var crosshatchPattern = '<rect x="' + (x - cs / 2) + '" y="' + (y - cs / 2) + '" width="' + ((w * cs) + cs) + '" height="' + ((h * cs) + cs) + '" style="fill:url(#crosshatch);" />';
let shadow = `M${x},${y} h${w * cs} v${h * cs} h${-w * cs} Z `;
return {room: RoomTxt, crosshatch: crosshatchPattern, shadow: shadow};
roomData = CreateRoom(xTile * cs, yTileN * cs, xW, yH);
dungeon += roomData.room;
crosshatch += roomData.crosshatch;
shadow += roomData.shadow;
shadow = shadow + '"style="fill:none;stroke:#000000;stroke-width:' + (cs - 5) + ';stroke-opacity:0.25;" />';
dungeon = document.getElementById("Random").innerHTML = '<svg width="' + ((maxX + 6) * cs) + '" height="' + (maxY * cs) + '"><defs><pattern id="crosshatch" patternUnits="userSpaceOnUse" width="100" height="100">' + generateCrosshatchPattern() + '</pattern></defs>' + crosshatch + shadow + dungeon + '</svg>';
Ultimate Dungeon Map Generator
But at the end of the day, it's just only a string of rooms separated with corridors, but we want to improve it.
We could try to implement one of the multiple dungeon generation algoriths available on the net or try an easy hack. As this is just a tutorial, we'll go with the easier and dirtiers solution.
We are going add up and down branches if we have enought space between the current position and the border of our image. To do so we'll add a few additiona room genetion in our main loop
dungeon += createDoor(xTile * cs, yTile * cs);
if (newPos.yTile > 9 && Math.random() > 0.5 && newPos.xTile < (maxX - 3)) {
branchPos = makeRandomCorridor(newPos.xTile + Math.floor(Math.random() * newPos.xW), newPos.yTile - 1, "up");
let branchX = branchPos.xTile;
let branchY = branchPos.yTile;
branchPos = makeRandomRoom(branchX, branchY);
dungeon += createDoor(branchX * cs, branchY * cs);
}
if (newPos.yTile + newPos.yH < (maxY-10) && Math.random() > 0.5 && newPos.xTile < (maxX - 3)) {
branchPos = makeRandomCorridor(newPos.xTile + Math.floor(Math.random() * newPos.xW), newPos.yTile + newPos.yH, "down");
let branchX = branchPos.xTile;
let branchY = branchPos.yTile;
branchPos = makeRandomRoom(branchX, branchY + 1);
dungeon += createDoor(branchX* cs, (branchY + 1) * cs);
}
xTile += newPos.xW;
First we updated our doors code, just returning an door for a horizontal or vertical conection.
function createDoor(x, y, direction) {
const doorWidth = cs;
const doorHeight = 4;
if (direction === 'horizontal') {
return `<rect x="${x-2}" y="${y + 3 }" width="${doorHeight}" height="${cs-6}" style="fill:white;stroke:black;stroke-width:2;" />`;
} else {
return `<rect x="${x + cs * 0.5 - doorWidth * 0.5}" y="${y-2}" width="${doorWidth}" height="${doorHeight}" style="fill:white;stroke:black;stroke-width:2;" />`;
}
}
function makeRandomCorridor(xTile, yTile, direction) {
let xW, yH, roomData;
if (direction === 'right') {
xW = Math.floor(2 + Math.random() * 4);
yH = 1;
roomData = CreateRoom(xTile * cs, yTile * cs, xW, yH);
dungeon += roomData.room;
dungeon += createDoor(xTile * cs, yTile * cs, 'horizontal');
} else if (direction === 'down') {
xW = 1;
yH = Math.floor(2 + Math.random() * 4);
roomData = CreateRoom(xTile * cs, yTile * cs, xW, yH);
dungeon += roomData.room;
dungeon += createDoor(xTile * cs, yTile * cs, 'vertical');
} else if (direction === 'up') {
xW = 1;
yH = Math.floor(2 + Math.random() * 4);
roomData = CreateRoom(xTile * cs, (yTile - yH + 1) * cs, xW, yH);
dungeon += roomData.room;
dungeon += createDoor(xTile * cs, (yTile + 1) * cs, 'vertical');
}
crosshatch += roomData.crosshatch;
shadow += roomData.shadow;
if (direction === 'right') {
return { xTile: xTile + xW, yTile };
} else if (direction === 'down') {
return { xTile: xTile, yTile: yTile + yH - 1 };
} else if (direction === 'up') {
return { xTile: xTile, yTile: yTile - yH + 1 };
}
}
function makeRandomRoom(xTile, yTile, direction) {
let xW, yH, xTileN, yTileN, roomData;
xW = Math.floor(3 + Math.random() * 4);
yH = Math.floor(3 + Math.random() * 4);
if (direction === 'right') {
yTileN = Math.min(Math.max(yTile - yH + 1 + Math.floor(Math.random() * yH), 1), maxY - yH);
roomData = CreateRoom(xTile * cs, yTileN * cs, xW, yH);
} else if (direction === 'down') {
xW = xW - 1;
yH = yH - 1;
xTileN = Math.min(Math.max(xTile - xW + 1 + Math.floor(Math.random() * xW), 1), maxX - xW);
roomData = CreateRoom(xTileN * cs, yTile * cs, xW, yH);
xTile = xTileN;
} else if (direction === 'up') {
xW = xW - 1;
yH = yH - 1;
xTileN = Math.min(Math.max(xTile - xW + 1 + Math.floor(Math.random() * xW), 1), maxX - xW);
roomData = CreateRoom(xTileN * cs, (yTile - yH) * cs, xW, yH);
xTile = xTileN;
}
dungeon += roomData.room;
crosshatch += roomData.crosshatch;
shadow += roomData.shadow;
return { xTile: xTile, yTile: yTileN, xW: xW, yH: yH };
}
Ultimate Dungeon Map Generator
See you in part six.
Comments
Post a Comment