Quick, dirty and ugly JavaScript tutorial for random generators (Part 5)

Random Generator Tutorial (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;
And in our CreateRoom funtion change the starting value of yTile, so we'll start in the middle of our larger map.
var yTile = Math.floor(maxY/2);
Also we need to replace all the instances of hardcoded values like the limit on our while loop.
while (xTile < maxX - 6) {
We'll also make other minor changes like moving the code to make rooms and corridors to their own functions and adding a small rectangles to display doors.
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;
}

${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)" />';
Also, we are going to replace the return line of our CreateRoom function to return us three variables, stored in a neat object.
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};
New we need to modify all the calls to CreateRoom to take into account that we get 3 strings instead of one. For example, like this:
roomData = CreateRoom(xTile * cs, yTileN * cs, xW, yH);
dungeon += roomData.room;
crosshatch += roomData.crosshatch;
shadow += roomData.shadow;
And lastly render all this new parts of the graphics at the end of our main function.
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>';
Now our dungeon has a nicer looking aspect!

Ultimate Dungeon Map Generator

Now our dungeon looks sexy!
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;
But makes a mess as our corridors, rooms and doors were designed to be added always left to right. So we need to add a parameter to pass the functions the orientation.
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;" />`;
  } }
Next we should update the corridors to be drawn vertical or horizontal. The veritcal ones need to take into account that the start and end points are computed diferently if they are go up or down.
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 };
  }
}
Now for the room placement too, well make rooms in the new branches a bit smaller than the ones in the main one.
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 };
}
The finished code could be found at: https://github.com/fmunoz-geo/MiniDungeonGen/blob/main/minidungeongen.html

Ultimate Dungeon Map Generator

See you in part six.

Comments

Popular posts from this blog

Random Character Generator for Warhammer Rolepaying Game 4th edition

WFRP4 NPC Generator

Treasure Generator