progress
This commit is contained in:
@@ -41,7 +41,7 @@ export class SourceCell {
|
|||||||
potentialNeighbours(other, direction) {
|
potentialNeighbours(other, direction) {
|
||||||
// sadly, we're not allowed to be friends with ourselves.
|
// sadly, we're not allowed to be friends with ourselves.
|
||||||
if (this === other) {
|
if (this === other) {
|
||||||
console.log("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
|
console.warn("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
|
||||||
// throw new Error("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
|
// throw new Error("WTF were checking to be friends with ourselves!", { _this: this, other, direction });
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ export class WfcCell {
|
|||||||
*/
|
*/
|
||||||
constructor(i, x, y, options) {
|
constructor(i, x, y, options) {
|
||||||
if (!options.length) {
|
if (!options.length) {
|
||||||
console.log("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
|
console.warn("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
|
||||||
throw Error("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
|
throw Error("Bad >>options<< arg in WfcCell constructor. Must not be empty.", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(options[0] instanceof SourceCell)) {
|
if (!(options[0] instanceof SourceCell)) {
|
||||||
console.log("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
|
console.warn("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
|
||||||
throw Error("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
|
throw Error("Bad >>options<< arg in WfcCell constructor. Must be array of WfcCells, but wasn't.", options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export class WfcGrid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
console.log("Resetting Cells");
|
|
||||||
const [w, h] = [this.width, this.height];
|
const [w, h] = [this.width, this.height];
|
||||||
const len = w * h;
|
const len = w * h;
|
||||||
this.cells = [];
|
this.cells = [];
|
||||||
@@ -44,7 +43,6 @@ export class WfcGrid {
|
|||||||
|
|
||||||
this.cells.push(new WfcCell(i, x, y, this.sourceGrid.clone().cells));
|
this.cells.push(new WfcCell(i, x, y, this.sourceGrid.clone().cells));
|
||||||
}
|
}
|
||||||
console.log("Done");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,7 +85,7 @@ export class WfcGrid {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (this.lowEntropyCellIdCache.length === 0) {
|
if (this.lowEntropyCellIdCache.length === 0) {
|
||||||
console.log("Found zero lowest-entropy cells.", { entropy: this.lowestEntropy });
|
console.info("Found zero lowest-entropy cells.", { entropy: this.lowestEntropy });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,8 @@
|
|||||||
}
|
}
|
||||||
#minimap {
|
#minimap {
|
||||||
grid-area: minimap;
|
grid-area: minimap;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
line-height: 11.5px;
|
line-height: 13px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
@@ -96,46 +96,44 @@
|
|||||||
<div id="minimap"></div>
|
<div id="minimap"></div>
|
||||||
<div id="mapInput">
|
<div id="mapInput">
|
||||||
<textarea id="mapText" rows="10" wrap="off">
|
<textarea id="mapText" rows="10" wrap="off">
|
||||||
############################################################
|
#########################################################
|
||||||
############################################################
|
# ################# ######################
|
||||||
############################################################
|
### # ################### # ## ######################
|
||||||
## # ################# ########################
|
# P _Z###############Z_ # ## ############## ::: P(east) _(portalA,west) Z(portalB) Z(portalA) _(portalB,west)
|
||||||
#### # ################### # ## ########################
|
# ######################## # ## #### ##
|
||||||
## P Z###############Z # ## ################ ::: P(east) Z(a, b, west) Z(b, a, east) // Comments
|
## E ################# # # ## # #### # # ## ::: E(gnoll)
|
||||||
## ######################## # ## #### ####
|
### ################## ## #### # ##
|
||||||
### E # # ## # #### # # #### ::: E(gnoll)
|
#### ################### # ## # # #### ##
|
||||||
#### ################## ## #### # ####
|
#####E#################### # ## ::: E(skelebones)
|
||||||
##### ################### # ## # # #### ####
|
##### #################### ########## #### ##
|
||||||
######E#################### # #### ::: E(Goblins, gnoll) // These are comments
|
##### #################### ########## # # #### # # ##
|
||||||
###### #################### ########## #### ####
|
##### #################### ########## #### # # ##
|
||||||
###### #################### ########## # # #### # # ####
|
##### #################### #################### ##
|
||||||
###### #################### ########## #### # # ####
|
##### #################### ##########################
|
||||||
###### #################### #################### ####
|
##### #################### # ##########################
|
||||||
###### #################### ############################
|
#####E#################### # ########################## ::: E(gnoll)
|
||||||
###### #################### # ############################
|
##### ###############_#### # ########################## ::: _(portalC, south)
|
||||||
###### #################### # ############################
|
##### ## ##### ## ########################## :::
|
||||||
######E#################### # ############################ ::: E(gnoll)
|
##### ## Z#### ## # # ########################## ::: Z(portalC)
|
||||||
###### ## ##### ## ############################ :::
|
##### ## _####Z ## ######## ########## ::: _(portalD, west) Z(portalD)
|
||||||
###### ## Z#### ## # # ############################ ::: Z(c, d, west)
|
##### ## ## # ########### ## ######## ##########
|
||||||
###### ## ####Z ## ######## ############ ::: Z(d, c, east)
|
#####E## # #E ########## ::: E(Dwarves, gnoll) E(Gelatinous_Cube, gnoll)
|
||||||
###### ## ## # ########### ## ######## ############
|
##### # # # ##########
|
||||||
######E## # #E ############ ::: E(Dwarves, gnoll) ; E(Gelatinous_Cube, gnoll)
|
######## # ## ########### # ######### # ##########
|
||||||
###### # # # ############
|
######## # # ########### # ######### # # ##########
|
||||||
######### # ## ########### # ######### # ############
|
######## ########### # ######### ##########
|
||||||
######### # # ########### # ######### # # ############
|
##########Z############### # ######### #### ############# ::: Z(portalE)
|
||||||
######### ########### # ######### ############
|
########################## # ######### #### #############
|
||||||
###########Z############### # ######### #### ############### ::: Z(e, f, null)
|
########################## # ######### #### #############
|
||||||
########################### # ######### #### ###############
|
########################## # ######### #### #############
|
||||||
########################### # ######### #### ###############
|
########################## # #### #############
|
||||||
########################### # ######### #### ###############
|
######################## # #### # # # ######## #
|
||||||
########################### # #### ###############
|
#######################_ # # ######## # # ::: _e(portalE, east)
|
||||||
######################### # #### # # # ######## ###
|
######################## # ##### # # # # ######## #
|
||||||
########################Z # # ######## # ### ::: Z(f, null, east) // you can teleport here, but you cannot teleport back
|
######################## # # #
|
||||||
######################### # ##### # # # # ######## ###
|
######################## ####################### # #
|
||||||
######################### # # ###
|
#################################################### #
|
||||||
######################### ####################### # ###
|
#########################################################
|
||||||
##################################################### ###
|
|
||||||
############################################################
|
|
||||||
</textarea
|
</textarea
|
||||||
>
|
>
|
||||||
<button onclick="game.loadMap()">Load Map</button>
|
<button onclick="game.loadMap()">Load Map</button>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
set orientation(o) {
|
set orientation(o) {
|
||||||
|
console.log({ o });
|
||||||
//
|
//
|
||||||
// Sanitize o
|
// Sanitize o
|
||||||
o = ((o | 0) + 4) % 4;
|
o = ((o | 0) + 4) % 4;
|
||||||
@@ -137,19 +138,23 @@ class DungeonCrawler {
|
|||||||
* @param {number} angle the orientation of the camera in radians around the unit circle.
|
* @param {number} angle the orientation of the camera in radians around the unit circle.
|
||||||
*/
|
*/
|
||||||
render(camX = this.player.x, camY = this.player.y, angle = this.player.angle) {
|
render(camX = this.player.x, camY = this.player.y, angle = this.player.angle) {
|
||||||
if (!this.rendering.firstPersonRenderer) {
|
if (!(this.rendering.firstPersonRenderer && this.rendering.firstPersonRenderer.ready)) {
|
||||||
console.log("Renderer not ready yet");
|
console.warn("Renderer not ready yet");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
queueMicrotask(() => {
|
||||||
this.rendering.firstPersonRenderer.renderFrame(
|
this.rendering.firstPersonRenderer.renderFrame(
|
||||||
camX, // add .5 to get camera into center of cell
|
camX, // add .5 to get camera into center of cell
|
||||||
camY, // add .5 to get camera into center of cell
|
camY, // add .5 to get camera into center of cell
|
||||||
angle,
|
angle,
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMinimap() {
|
renderMinimap() {
|
||||||
|
queueMicrotask(() => {
|
||||||
this.rendering.miniMapRenderer.draw(this.player.x, this.player.y, this.player.orientation);
|
this.rendering.miniMapRenderer.draw(this.player.x, this.player.y, this.player.orientation);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMap() {
|
loadMap() {
|
||||||
@@ -158,6 +163,8 @@ class DungeonCrawler {
|
|||||||
this.map = TileMap.fromHumanText(mapString);
|
this.map = TileMap.fromHumanText(mapString);
|
||||||
|
|
||||||
this.player._posV = this.map.findFirstV({ isStartLocation: true });
|
this.player._posV = this.map.findFirstV({ isStartLocation: true });
|
||||||
|
this.player.orientation = this.map.findFirstTile({ isStartLocation: true }).orientation;
|
||||||
|
console.log(this.player);
|
||||||
|
|
||||||
if (!this.player._posV) {
|
if (!this.player._posV) {
|
||||||
throw new Error("Could not find a start location for the player");
|
throw new Error("Could not find a start location for the player");
|
||||||
@@ -236,8 +243,8 @@ class DungeonCrawler {
|
|||||||
// Bumping into a door will open/remove it.
|
// Bumping into a door will open/remove it.
|
||||||
// Bumping into stairs will go down/up (requires confirmation, unless disabled)
|
// Bumping into stairs will go down/up (requires confirmation, unless disabled)
|
||||||
// Bumping into a wall sconce will pick up the torch (losing the light on the wall, but gaining a torch that lasts for X turns)
|
// Bumping into a wall sconce will pick up the torch (losing the light on the wall, but gaining a torch that lasts for X turns)
|
||||||
// Bumping into a trap activates it.
|
// Bumping into a trap activates it (or reveals it if someone on the team detects it, or of a detect trap spell is running)
|
||||||
// Bumping into a treasure opens it.
|
// Bumping into loot reveals it
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
"bumped into %s at %s (mypos: %s), direction=%d",
|
"bumped into %s at %s (mypos: %s), direction=%d",
|
||||||
@@ -328,10 +335,10 @@ class DungeonCrawler {
|
|||||||
//
|
//
|
||||||
// Guard: stop animation if it took too long
|
// Guard: stop animation if it took too long
|
||||||
if (this.animation.targetTime <= performance.now()) {
|
if (this.animation.targetTime <= performance.now()) {
|
||||||
|
this.animation = {};
|
||||||
this.render(this.player.x, this.player.y, this.player.angle);
|
this.render(this.player.x, this.player.y, this.player.angle);
|
||||||
this.renderMinimap();
|
this.renderMinimap();
|
||||||
this.renderStatus();
|
this.renderStatus();
|
||||||
this.animation = {};
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,7 +406,7 @@ class DungeonCrawler {
|
|||||||
renderStatus() {
|
renderStatus() {
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
// Update the compass
|
// Update the compass and status
|
||||||
document.getElementById("status").innerHTML = sprintf(
|
document.getElementById("status").innerHTML = sprintf(
|
||||||
[
|
[
|
||||||
"<div>",
|
"<div>",
|
||||||
|
|||||||
@@ -82,23 +82,48 @@ export class FirstPersonRenderer {
|
|||||||
/** @type {THREE.Sprite[]} All roaming tiles that regularly needs their positions updated */
|
/** @type {THREE.Sprite[]} All roaming tiles that regularly needs their positions updated */
|
||||||
this.roamers = [];
|
this.roamers = [];
|
||||||
|
|
||||||
|
/** @type {number} how many asynchronous function returns are we waiting for? */
|
||||||
|
this.openAsyncs = 0;
|
||||||
|
/** @type {boolean} Are we ready to render? (have all resources been loaded?) */
|
||||||
|
this.ready = false;
|
||||||
|
/** @type {function} called when the renderer is ready and all resources have been loaded */
|
||||||
|
this.onReady = null;
|
||||||
|
|
||||||
//
|
//
|
||||||
this.initMap();
|
this.initMap();
|
||||||
|
|
||||||
//
|
//
|
||||||
this.renderer.setSize(this.asciiWidth * 1, this.asciiHeight * 1);
|
this.renderer.setSize(this.asciiWidth * 1, this.asciiHeight * 1);
|
||||||
|
|
||||||
|
const waitForAsyncs = () => {
|
||||||
|
if (this.ready) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.openAsyncs > 0) {
|
||||||
|
setTimeout(waitForAsyncs, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ready = true;
|
||||||
|
if (typeof this.onReady === "function") {
|
||||||
|
this.onReady();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.renderFrame();
|
this.renderFrame();
|
||||||
|
};
|
||||||
|
setTimeout(waitForAsyncs, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTexture(textureId) {
|
getTexture(textureId) {
|
||||||
console.debug("fetching texture", { textureId });
|
|
||||||
let texture = this.textures.get(textureId);
|
let texture = this.textures.get(textureId);
|
||||||
if (!texture) {
|
if (!texture) {
|
||||||
console.debug(" miss... loading texture", { textureId });
|
this.openAsyncs++;
|
||||||
texture = new THREE.TextureLoader().load(`${textureId}.png`, (t) => {
|
texture = new THREE.TextureLoader().load(`${textureId}.png`, (t) => {
|
||||||
t.magFilter = THREE.NearestFilter; // no smoothing when scaling up
|
t.magFilter = THREE.NearestFilter; // no smoothing when scaling up
|
||||||
t.minFilter = THREE.NearestFilter; // no mipmaps / no smoothing when scaling down
|
t.minFilter = THREE.NearestFilter; // no mipmaps / no smoothing when scaling down
|
||||||
t.generateMipmaps = false; // don’t build mipmaps
|
t.generateMipmaps = false; // don’t build mipmaps
|
||||||
|
this.openAsyncs--;
|
||||||
});
|
});
|
||||||
this.textures.set(textureId, texture);
|
this.textures.set(textureId, texture);
|
||||||
}
|
}
|
||||||
@@ -111,12 +136,9 @@ export class FirstPersonRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSpriteMaterial(textureId) {
|
getSpriteMaterial(textureId) {
|
||||||
console.debug("fetching material", { textureId });
|
|
||||||
|
|
||||||
let material = this.spriteMaterials.get(textureId);
|
let material = this.spriteMaterials.get(textureId);
|
||||||
|
|
||||||
if (!material) {
|
if (!material) {
|
||||||
console.log("Creating material", { textureId });
|
|
||||||
material = new THREE.SpriteMaterial({
|
material = new THREE.SpriteMaterial({
|
||||||
map: this.getTexture(textureId),
|
map: this.getTexture(textureId),
|
||||||
transparent: true,
|
transparent: true,
|
||||||
@@ -150,7 +172,6 @@ export class FirstPersonRenderer {
|
|||||||
this.mainCamera.lookAt(x, y - 1, 0);
|
this.mainCamera.lookAt(x, y - 1, 0);
|
||||||
this.torch.position.copy(this.mainCamera.position);
|
this.torch.position.copy(this.mainCamera.position);
|
||||||
|
|
||||||
console.log("Initial Camera Position:", this.mainCamera.position);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +205,8 @@ export class FirstPersonRenderer {
|
|||||||
// ---------------------------
|
// ---------------------------
|
||||||
const floorGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
|
const floorGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
|
||||||
const floorMat = new THREE.MeshStandardMaterial({
|
const floorMat = new THREE.MeshStandardMaterial({
|
||||||
color: this.floorColor /* side: THREE.DoubleSide */,
|
color: this.floorColor,
|
||||||
|
/* side: THREE.DoubleSide */
|
||||||
});
|
});
|
||||||
const floor = new THREE.Mesh(floorGeo, floorMat);
|
const floor = new THREE.Mesh(floorGeo, floorMat);
|
||||||
floor.position.set(this.map.width / 2, this.map.height / 2, -0.5);
|
floor.position.set(this.map.width / 2, this.map.height / 2, -0.5);
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export class MiniMap {
|
|||||||
* @param {Orientation} orientation
|
* @param {Orientation} orientation
|
||||||
*/
|
*/
|
||||||
draw(pX, pY, orientation) {
|
draw(pX, pY, orientation) {
|
||||||
console.log("Updating minimap", { px: pX, py: pY, orientation });
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// 2D array of tiles that are visible
|
// 2D array of tiles that are visible
|
||||||
const visibleTiles = new Array(this.map.height).fill().map(() => new Array(this.map.width).fill(false));
|
const visibleTiles = new Array(this.map.height).fill().map(() => new Array(this.map.width).fill(false));
|
||||||
@@ -151,12 +149,12 @@ export class MiniMap {
|
|||||||
invertY = true;
|
invertY = true;
|
||||||
break;
|
break;
|
||||||
case Orientation.EAST:
|
case Orientation.EAST:
|
||||||
|
invertY = true;
|
||||||
|
invertX = true;
|
||||||
switchXY = true;
|
switchXY = true;
|
||||||
break;
|
break;
|
||||||
case Orientation.WEST:
|
case Orientation.WEST:
|
||||||
switchXY = true;
|
switchXY = true;
|
||||||
invertY = true;
|
|
||||||
invertX = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,35 @@
|
|||||||
import parseOptions, { ParsedCall } from "../utils/callParser.js";
|
import parseOptions, { TileOptions } from "../utils/tileOptionsParser.js";
|
||||||
import { Tile } from "./ascii_tile_types.js";
|
import { Tile, WallTile } from "./ascii_tile_types.js";
|
||||||
import { Vector2i } from "./ascii_types.js";
|
import { Vector2i } from "./ascii_types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} TileWithCoords
|
||||||
|
* @property {Tile} tile
|
||||||
|
* @property {number} x
|
||||||
|
* @property {number} y
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Map<number,TileWithCoords>} TileCoordsHashTable
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback TileMapForEachCallback
|
||||||
|
* @param {Tile} tile
|
||||||
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @returns {undefined|any} If undefined is returned, the looping continues, but if anything else is returned, the loop halts, and the return value is passed along to the caller
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly @constant @enum {string}
|
||||||
|
*/
|
||||||
|
export const CharType = {
|
||||||
|
SYSTEM: "internalMapChar",
|
||||||
|
MINIMAP: "minimapChar",
|
||||||
|
MINIMAP_REVEALED: "revealedMinimapChar",
|
||||||
|
};
|
||||||
|
|
||||||
export class TileMap {
|
export class TileMap {
|
||||||
/**
|
/**
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
@@ -22,7 +50,6 @@ export class TileMap {
|
|||||||
if (y === 0) {
|
if (y === 0) {
|
||||||
// Infer the width of the map from the first line
|
// Infer the width of the map from the first line
|
||||||
mapWidth = tileStr.length;
|
mapWidth = tileStr.length;
|
||||||
console.log({ mapWidth });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new row in the 2d tiles array
|
// Create a new row in the 2d tiles array
|
||||||
@@ -32,8 +59,6 @@ export class TileMap {
|
|||||||
const options = optionStr ? parseOptions(optionStr) : [];
|
const options = optionStr ? parseOptions(optionStr) : [];
|
||||||
let lineWidth = 0;
|
let lineWidth = 0;
|
||||||
|
|
||||||
options.length && console.log({ options, y });
|
|
||||||
|
|
||||||
tileStr.split("").forEach((char, x) => {
|
tileStr.split("").forEach((char, x) => {
|
||||||
//
|
//
|
||||||
// Check if there are options in the queue that matches the current character
|
// Check if there are options in the queue that matches the current character
|
||||||
@@ -59,26 +84,26 @@ export class TileMap {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Tile[][]} tiles
|
* @param {Tile[][]} tiles
|
||||||
* @param {Map<Tile,ParsedCall>} options
|
|
||||||
*/
|
*/
|
||||||
constructor(tiles) {
|
constructor(tiles) {
|
||||||
/** @constant @readonly @type {number} */
|
/** @type {number} */ this.height = tiles.length;
|
||||||
this.height = tiles.length;
|
/** @type {number} */ this.width = tiles[0].length;
|
||||||
/** @constant @readonly @type {number} */
|
/** @type {Tile[][]} */ this.tiles = tiles;
|
||||||
this.width = tiles[0].length;
|
/** @type {number} */ this.playerStartX = undefined;
|
||||||
/** @constant @readonly @type {Tile[][]} */
|
/** @type {number} */ this.playerStartT = undefined;
|
||||||
this.tiles = tiles;
|
/** @type {Tile} */ this.outOfBoundsWall = this.getReferenceWallTile();
|
||||||
|
|
||||||
/** @type {Tile} when probing a coordinate outside the map, this is the tile that is returned */
|
|
||||||
this.outOfBoundsWall = this.findFirstV({ looksLikeWall: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
/**
|
||||||
|
* @param {CharType} charType
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
toString(charType = CharType.SYSTEM) {
|
||||||
let result = "";
|
let result = "";
|
||||||
for (let y = 0; y < this.height; y++) {
|
for (let y = 0; y < this.height; y++) {
|
||||||
for (let x = 0; x < this.width; x++) {
|
for (let x = 0; x < this.width; x++) {
|
||||||
const tile = this.tiles[y][x];
|
const tile = this.tiles[y][x];
|
||||||
result += tile.minimapChar;
|
result += tile[charType];
|
||||||
}
|
}
|
||||||
result += "\n";
|
result += "\n";
|
||||||
}
|
}
|
||||||
@@ -96,12 +121,12 @@ export class TileMap {
|
|||||||
return this.tiles[y][x];
|
return this.tiles[y][x];
|
||||||
}
|
}
|
||||||
|
|
||||||
get(x, y) {
|
get(x, y, outOfBounds = this.outOfBoundsWall) {
|
||||||
x |= 0;
|
x |= 0;
|
||||||
y |= 0;
|
y |= 0;
|
||||||
|
|
||||||
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
||||||
return this.outOfBoundsWall;
|
return outOfBounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.tiles[y][x];
|
return this.tiles[y][x];
|
||||||
@@ -178,7 +203,7 @@ export class TileMap {
|
|||||||
* but _stops_ if fn() returns anything but `undefined`,
|
* but _stops_ if fn() returns anything but `undefined`,
|
||||||
* and then that return value is returned from `forEach`
|
* and then that return value is returned from `forEach`
|
||||||
*
|
*
|
||||||
* @param { (tile, x,y) => any|undefined ) } fn
|
* @param {TileMapForEachCallback} fn
|
||||||
* @returns any|undefined
|
* @returns any|undefined
|
||||||
*/
|
*/
|
||||||
forEach(fn) {
|
forEach(fn) {
|
||||||
@@ -192,23 +217,82 @@ export class TileMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getArea(xMin, yMin, xMax, yMax) {
|
/**
|
||||||
if (xMin > xMax) {
|
* @returns {number}
|
||||||
[xMin, xMax] = [xMax, xMin];
|
*/
|
||||||
|
getTraversableTileCount() {
|
||||||
|
let sum = 0;
|
||||||
|
|
||||||
|
this.forEach((tile) => {
|
||||||
|
if (tile.isTraversable) {
|
||||||
|
sum++;
|
||||||
}
|
}
|
||||||
if (yMin > yMax) {
|
});
|
||||||
[yMin, yMax] = [yMax, yMin];
|
|
||||||
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = xMax - xMin + 1;
|
/**
|
||||||
const h = yMax - yMin + 1;
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @param {typeof Tile} tileClass
|
||||||
|
* @returns {TileWithCoords[]}
|
||||||
|
*/
|
||||||
|
getCardinalAdjacentTiles(x, y, tileClass) {
|
||||||
|
/** @type {TileWithCoords[]} */
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
const testCoords = [
|
||||||
|
[x + 1, y],
|
||||||
|
[x - 1, y],
|
||||||
|
[x, y + 1],
|
||||||
|
[x, y + 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [_x, _y] of testCoords) {
|
||||||
|
const _tile = this.get(_x, _y, false);
|
||||||
|
|
||||||
|
if (_tile === false) {
|
||||||
|
// _x, _y was out of bounds, do not add it to result
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tileClass && !(_tile instanceof tileClass)) {
|
||||||
|
// _tile was of invalid type, do not add it to result
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({ tile: _tile, x: _x, y: _y });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} minX
|
||||||
|
* @param {number} minY
|
||||||
|
* @param {number} maxX
|
||||||
|
* @param {number} maxY
|
||||||
|
*
|
||||||
|
* @returns {TileMap}
|
||||||
|
*/
|
||||||
|
getArea(minX, minY, maxX, maxY) {
|
||||||
|
if (minX > maxX) {
|
||||||
|
[minX, maxX] = [maxX, minX];
|
||||||
|
}
|
||||||
|
if (minY > maxY) {
|
||||||
|
[minY, maxY] = [maxY, minY];
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = maxX - minX + 1;
|
||||||
|
const h = maxY - minY + 1;
|
||||||
let iX = 0;
|
let iX = 0;
|
||||||
let iY = 0;
|
let iY = 0;
|
||||||
|
|
||||||
const tiles = new Array(h).fill().map(() => new Array(w));
|
const tiles = new Array(h).fill().map(() => new Array(w));
|
||||||
|
|
||||||
for (let y = yMin; y <= yMax; y++) {
|
for (let y = minY; y <= maxY; y++) {
|
||||||
for (let x = xMin; x <= xMax; x++) {
|
for (let x = minX; x <= maxX; x++) {
|
||||||
const tile = this.tiles[y][x];
|
const tile = this.tiles[y][x];
|
||||||
if (!tile) {
|
if (!tile) {
|
||||||
throw new Error("Dafuqq is happing here?");
|
throw new Error("Dafuqq is happing here?");
|
||||||
@@ -222,11 +306,68 @@ export class TileMap {
|
|||||||
return new TileMap(w, h, tiles);
|
return new TileMap(w, h, tiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAreaAround(x, y, radius) {
|
/**
|
||||||
return this.getArea(x - radius, y - radius, x + radius, y + radius);
|
* @param {number} x
|
||||||
|
* @param {number} y
|
||||||
|
* @param {number} manhattanRadius
|
||||||
|
*/
|
||||||
|
getAreaAround(x, y, manhattanRadius) {
|
||||||
|
return this.getArea(
|
||||||
|
x - manhattanRadius, // minX
|
||||||
|
y - manhattanRadius, // minY
|
||||||
|
x + manhattanRadius, // maxX
|
||||||
|
y + manhattanRadius, // maxY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} startX
|
||||||
|
* @param {number} startY
|
||||||
|
* @returns {TileCoordsHashTable}
|
||||||
|
*/
|
||||||
|
getAllTraversableTilesConnectedTo(startX, startY) {
|
||||||
|
/** @type {TileCoordsHashTable} */
|
||||||
|
const result = new Map();
|
||||||
|
|
||||||
|
const allTilesFlat = new Array(this.width * this.height).fill();
|
||||||
|
|
||||||
|
this.forEach((tile, x, y) => {
|
||||||
|
const idx = x + y * this.width;
|
||||||
|
allTilesFlat[idx] = { tile, x, y };
|
||||||
|
});
|
||||||
|
|
||||||
|
const inspectionStack = [startX + startY * this.width];
|
||||||
|
|
||||||
|
while (inspectionStack.length > 0) {
|
||||||
|
const idx = inspectionStack.pop();
|
||||||
|
|
||||||
|
const { tile, x, y } = allTilesFlat[idx];
|
||||||
|
|
||||||
|
if (!tile.isTraversable) {
|
||||||
|
continue; // Can't walk there, move on
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.has(idx)) {
|
||||||
|
continue; // Already been here, move on
|
||||||
|
}
|
||||||
|
|
||||||
|
result.set(idx, allTilesFlat[idx]);
|
||||||
|
|
||||||
|
// Add neighbors
|
||||||
|
const [minX, minY] = [1, 1];
|
||||||
|
const maxX = this.width - 2;
|
||||||
|
const maxY = this.height - 2;
|
||||||
|
|
||||||
|
if (y >= minY) inspectionStack.push(idx - this.width); // up
|
||||||
|
if (y <= maxY) inspectionStack.push(idx + this.width); // down
|
||||||
|
if (x >= minX) inspectionStack.push(idx - 1); // left
|
||||||
|
if (x <= maxX) inspectionStack.push(idx + 1); // right
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.PI < 0 && ParsedCall) {
|
if (Math.PI < 0 && TileOptions && WallTile) {
|
||||||
("STFU Linda");
|
("STFU Linda");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +1,211 @@
|
|||||||
import { ParsedCall } from "../utils/callParser";
|
import { mustBe, mustBeString } from "../utils/mustbe.js";
|
||||||
import { Orientation, Vector2i } from "./ascii_types";
|
import shallowCopy from "../utils/shallowCopy.js";
|
||||||
|
import { TileOptions } from "../utils/tileOptionsParser.js";
|
||||||
|
import { Orientation, Vector2i } from "./ascii_types.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of __internal__ characters used to identify tile types.
|
||||||
|
* These are __not__ necessarily the characters used to display
|
||||||
|
* the tile on the minimap - but they are used when serializing
|
||||||
|
* the maps into a semi-human-readable text-format.
|
||||||
|
*
|
||||||
|
* @constant {Record<string,string}
|
||||||
|
*/
|
||||||
|
export const TileChars = Object.freeze({
|
||||||
|
FLOOR: " ",
|
||||||
|
WALL: "#",
|
||||||
|
SECRET_PORTAL: "Z",
|
||||||
|
TELPORTATION_TARGET: "_",
|
||||||
|
ENCOUNTER_START_POINT: "E",
|
||||||
|
PLAYER_START_POINT: "P",
|
||||||
|
});
|
||||||
|
|
||||||
|
const REQUIRED_ID = Symbol("REQUIRED_ID");
|
||||||
|
const REQUIRED_ORIENTATION = Symbol("REQUIRED_ORIENTATION");
|
||||||
|
const REQUIRED_OCCUPANTS = Symbol("REQUIRED_OCCUPANTS");
|
||||||
|
|
||||||
|
/** @type {Record<string,Tile>} */
|
||||||
|
export const TileTypes = {
|
||||||
|
[TileChars.FLOOR]: {
|
||||||
|
minimapChar: "·",
|
||||||
|
traversable: true,
|
||||||
|
},
|
||||||
|
[TileChars.WALL]: {
|
||||||
|
minimapChar: "█",
|
||||||
|
minimapColor: "#aaa",
|
||||||
|
textureId: "wall",
|
||||||
|
traversable: false,
|
||||||
|
looksLikeWall: true,
|
||||||
|
},
|
||||||
|
[TileChars.SECRET_PORTAL]: {
|
||||||
|
disguiseAs: TileChars.WALL,
|
||||||
|
revealedMinimapChar: "Ω",
|
||||||
|
revealedMinimapColor: "#EE82EE", //purple
|
||||||
|
revealedTextureId: "secret_portal_revealed",
|
||||||
|
portalTargetId: REQUIRED_ID,
|
||||||
|
looksLikeWall: true,
|
||||||
|
},
|
||||||
|
[TileChars.TELPORTATION_TARGET]: {
|
||||||
|
is: TileChars.FLOOR,
|
||||||
|
id: REQUIRED_ID,
|
||||||
|
orientation: REQUIRED_ORIENTATION,
|
||||||
|
disguiseAs: TileChars.FLOOR,
|
||||||
|
revealedMinimapChar: "𝑥",
|
||||||
|
revealedMinimapColor: "#EE82EE", // purple
|
||||||
|
},
|
||||||
|
[TileChars.ENCOUNTER_START_POINT]: {
|
||||||
|
is: TileChars.FLOOR, // this is actually just a floor tile that is occupied by an encounter when the map is loaded
|
||||||
|
encounterId: REQUIRED_ID,
|
||||||
|
textureId: REQUIRED_ID,
|
||||||
|
occupants: REQUIRED_OCCUPANTS,
|
||||||
|
},
|
||||||
|
[TileChars.PLAYER_START_POINT]: {
|
||||||
|
is: TileChars.FLOOR,
|
||||||
|
orientation: REQUIRED_ORIENTATION,
|
||||||
|
minimapChar: "▤", // stairs/ladder
|
||||||
|
minimapColor: "#FFF",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export class Tile {
|
export class Tile {
|
||||||
/** @type {string|number} What is the id of this tile - only interactive tiles have IDs */
|
/** @readonly {string?|number?} Unique (but optional) instance if of this tile */
|
||||||
id;
|
id;
|
||||||
|
|
||||||
/** @type {string} Icon char of tile */
|
/** @type {string} Icon char of tile */
|
||||||
minimapChar;
|
minimapChar;
|
||||||
/** @type {string} Icon char of tile after tile's secrets have been revealed */
|
/** @type {string} Color of the icon of tile */
|
||||||
revealedMinimapChar;
|
|
||||||
/** @type {string} Icon of tile */
|
|
||||||
minimapColor;
|
minimapColor;
|
||||||
/** @type {string} Icon char of tile after tile's secrets have been revealed */
|
/** @type {boolean} Can the player walk here? */
|
||||||
revealedMinimapColor;
|
isTraversable;
|
||||||
|
/** @type {boolean} Should this be rendered as a wall? */
|
||||||
|
looksLikeWall;
|
||||||
|
/** @type {boolean} Is this where they player starts? */
|
||||||
|
isStartLocation;
|
||||||
/** @type {boolean} Is this a portal exit and/or entry */
|
/** @type {boolean} Is this a portal exit and/or entry */
|
||||||
isPortal;
|
isPortal;
|
||||||
/** @type {string|number} Where is the player transported if they enter the portal */
|
/** @type {string|number} Where is the player transported if they enter the portal */
|
||||||
portalTargetId;
|
portalTargetId;
|
||||||
|
|
||||||
/** @type {boolean} Should this be rendered as a wall? */
|
|
||||||
looksLikeWall;
|
|
||||||
/** @type {boolean} Can the player walk here? */
|
|
||||||
isTraversable;
|
|
||||||
/** @type {boolean} is this tile occupied by an encounter? */
|
|
||||||
isEncounter;
|
|
||||||
/** @type {boolean} Is this where they player starts? */
|
|
||||||
isStartLocation;
|
|
||||||
/** @type {boolean} Has the secret properties of this tile been revealed? */
|
|
||||||
isRevealed;
|
|
||||||
/** @type {string|number} */
|
|
||||||
hasBumpEvent;
|
|
||||||
/** @type {string|number} The portals "channel" - each tile in a portal pair must have the same channel */
|
|
||||||
channel;
|
|
||||||
/** @type {number|string} id of texture to use */
|
/** @type {number|string} id of texture to use */
|
||||||
textureId;
|
textureId;
|
||||||
/** @type {number|string} id of texture to use after the secrets of this tile has been revealed */
|
|
||||||
revealedTextureId;
|
|
||||||
/** @type {number|string} type of encounter located on this tile. May or may not be unique*/
|
/** @type {number|string} type of encounter located on this tile. May or may not be unique*/
|
||||||
encounterType;
|
encounterType;
|
||||||
/** @type {boolean} Can/does this tile wander around on empty tiles? */
|
/** @type {number|string} type of trap located on this tile. May or may not be unique*/
|
||||||
isRoaming;
|
trapType;
|
||||||
/** @type {Orientation} */
|
/** @type {Orientation} */
|
||||||
orientation;
|
orientation;
|
||||||
/** @type {number} If this is a roaming tile, what is its current x-position on the map */
|
/** @type {TileType} This tile disguises itself as another tile, and its true properties are revealed later if event is triggered */
|
||||||
currentPosX;
|
disguiseAs;
|
||||||
/** @type {number} If this is a roaming tile, what is its current y-position on the map*/
|
/** @type {boolean} Has the secret properties of this tile been revealed? */
|
||||||
currentPosY;
|
revealed;
|
||||||
|
/** @type {string} Icon char of tile after tile's secrets have been revealed */
|
||||||
|
revealedMinimapChar;
|
||||||
|
/** @type {string} Color of the icon char of tile after tile's secrets have been revealed */
|
||||||
|
revealedMinimapColor;
|
||||||
|
/** @type {number|string} id of texture to use after the secrets of this tile has been revealed */
|
||||||
|
revealedTextureId;
|
||||||
|
|
||||||
static wallMinimapChar = "█";
|
/** @param {Tile} properties */
|
||||||
|
constructor(properties) {
|
||||||
|
mustBe(properties, "object");
|
||||||
|
|
||||||
/** @param {Tile} options */
|
//
|
||||||
constructor(options = {}) {
|
// Copy props from properties.
|
||||||
for (let [k, v] of Object.entries(options)) {
|
//
|
||||||
if (this[k] !== undefined) {
|
for (const [key, val] of Object.entries(properties)) {
|
||||||
this[k] = v;
|
if (typeof val === "symbol" && val.description.startsWith("REQUIRED_")) {
|
||||||
|
console.error(
|
||||||
|
[
|
||||||
|
"REQUIRED_ symbol encountered in Tile constructor. ",
|
||||||
|
"REQUIRED_ is a placeholder, and cannot be used as a value directly",
|
||||||
|
].join("\n"),
|
||||||
|
{ key, val, options: properties },
|
||||||
|
);
|
||||||
|
throw new Error("Incomplete data in constructor. Args may not contain a data placeholder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!Object.hasOwn(this, key) /* Object.prototype.hasOwnProperty.call(this, key) */) {
|
||||||
|
console.warn("Unknown tile property", { key, val, properties });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// If this tile is disguised, copy its attributes, but
|
||||||
|
// do not overwrite own attributes.
|
||||||
|
//
|
||||||
|
if (this.disguiseAs !== undefined) {
|
||||||
|
this.revealed = false;
|
||||||
|
|
||||||
|
const other = shallowCopy(TileTypes[this.is]);
|
||||||
|
for (const [pKey, pVal] of Object.entries(other)) {
|
||||||
|
if (this.key !== undefined) {
|
||||||
|
this[pKey] = pVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// If this tile "inherits" properties from another tile type,
|
||||||
|
// copy those properties, but do not overwrite own attributes.
|
||||||
|
//
|
||||||
|
if (this.is !== undefined) {
|
||||||
|
//
|
||||||
|
const other = shallowCopy(TileTypes[this.is]);
|
||||||
|
for (const [pKey, pVal] of Object.entries(other)) {
|
||||||
|
if (this.key !== undefined) {
|
||||||
|
this[pKey] = pVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Normalize Orientation
|
||||||
|
//
|
||||||
|
if (this.orientation !== undefined && typeof this.orientation === "string") {
|
||||||
|
const valueMap = {
|
||||||
|
north: Orientation.NORTH,
|
||||||
|
south: Orientation.SOUTH,
|
||||||
|
east: Orientation.EAST,
|
||||||
|
west: Orientation.WEST,
|
||||||
|
};
|
||||||
|
this.orientation = mustBeString(valueMap[this.orientation.toLowerCase()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.id !== undefined) {
|
||||||
|
mustBe(this.id, "number", "string");
|
||||||
|
}
|
||||||
|
if (this.textureId !== undefined) {
|
||||||
|
mustBe(this.textureId, "number", "string");
|
||||||
|
}
|
||||||
|
if (this.portalTargetId !== undefined) {
|
||||||
|
mustBe(this.portalTargetId, "number", "string");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} char
|
* @param {string} char
|
||||||
* @param {ParsedCall} opt Options
|
* @param {TileOptions} options Options
|
||||||
* @param {number} x
|
* @param {number} x
|
||||||
* @param {number} y
|
* @param {number} y
|
||||||
*/
|
*/
|
||||||
static fromChar(char, opt, x, y) {
|
static fromChar(char, options) {
|
||||||
opt = opt ?? new ParsedCall();
|
//
|
||||||
if (!(opt instanceof ParsedCall)) {
|
// Validate Options
|
||||||
console.error("Invalid options", { char, opt: opt });
|
options = options ?? new TileOptions();
|
||||||
|
if (!(options instanceof TileOptions)) {
|
||||||
|
console.error("Invalid options", { char, opt: options });
|
||||||
throw new Error("Invalid options");
|
throw new Error("Invalid options");
|
||||||
}
|
}
|
||||||
if (char === " ") return new FloorTile();
|
|
||||||
if (char === "#") return new WallTile();
|
|
||||||
if (char === "P") return new PlayerStartTile(opt.getValue("orientation", 0));
|
|
||||||
if (char === "E")
|
|
||||||
return new EncounterTile(x, y, opt.getValue("encounterType", 0), opt.getValue("textureId", 1));
|
|
||||||
if (char === "Z")
|
|
||||||
return new SecretPortalTile(
|
|
||||||
opt.getValue("id", 0),
|
|
||||||
opt.getValue("destinationid", 1),
|
|
||||||
opt.getValue("orientation", 3),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.warn("Unknown character", { char, options: opt });
|
const typeInfo = TileTypes[char];
|
||||||
return new FloorTile();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasTexture() {
|
let optionPos = 0;
|
||||||
if (typeof this.textureId === "number") {
|
const creationArgs = {};
|
||||||
return true;
|
const getOption = (name) => options.getValue(name, optionPos++);
|
||||||
}
|
for (let [key, val] of Object.entries(typeInfo)) {
|
||||||
if (typeof this.textureId === "string" && this.textureId !== "") {
|
//
|
||||||
return true;
|
const fetchFromOption = typeof val === "symbol" && val.descript.startsWith("REQUIRED_");
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
creationArgs[key] = fetchFromOption ? getOption(key) : shallowCopy(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBumpEvent() {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
@@ -107,77 +213,6 @@ export class Tile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FloorTile extends Tile {
|
if (Math.PI < 0 && TileOptions && Orientation && Vector2i) {
|
||||||
isTraversable = true;
|
|
||||||
minimapChar = "·";
|
|
||||||
minimapColor = "#555";
|
|
||||||
internalMapChar = " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PlayerStartTile extends Tile {
|
|
||||||
isTraversable = true;
|
|
||||||
isStartLocation = true;
|
|
||||||
minimapChar = "▤"; // stairs
|
|
||||||
orientation = Orientation.NORTH;
|
|
||||||
|
|
||||||
/** @param {Orientation} orientation */
|
|
||||||
constructor(orientation) {
|
|
||||||
super({ orientation });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WallTile extends Tile {
|
|
||||||
textureId = "wall";
|
|
||||||
isTraversable = false;
|
|
||||||
looksLikeWall = true;
|
|
||||||
internalMapChar = "#";
|
|
||||||
minimapChar = Tile.wallMinimapChar;
|
|
||||||
minimapColor = "#aaa";
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EncounterTile extends Tile {
|
|
||||||
isEncounter = true;
|
|
||||||
isRoaming = true;
|
|
||||||
minimapChar = "†";
|
|
||||||
minimapColor = "#f44";
|
|
||||||
hasBumpEvent = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} x x-component of the encounter's initial position
|
|
||||||
* @param {number} y y-component of the encounter's initial position
|
|
||||||
* @param {string|number} encounterType name/id of the encounter that will be triggered when player bumps into this tile
|
|
||||||
* @param {string|number} textureId id of the texture to use.
|
|
||||||
*/
|
|
||||||
constructor(x, y, encounterType, textureId) {
|
|
||||||
super();
|
|
||||||
this.textureId = textureId ?? encounterType;
|
|
||||||
this.encounterType = encounterType;
|
|
||||||
this.currentPosX = x;
|
|
||||||
this.currentPosY = y;
|
|
||||||
this.id = `E_${encounterType}_${x}_${y}`;
|
|
||||||
console.info("creating encounter", { encounter: this });
|
|
||||||
}
|
|
||||||
|
|
||||||
getBumpEvent() {
|
|
||||||
return ["attack", { encounterType: this.encounterType }];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SecretPortalTile extends WallTile {
|
|
||||||
revealedTextureId = "secretTwoWayPortal";
|
|
||||||
isPortal = true;
|
|
||||||
internalMapChar = "Z";
|
|
||||||
isRevealed = false;
|
|
||||||
revealedMinimapChar = "Ω";
|
|
||||||
revealedMinimapColor = "#4f4";
|
|
||||||
|
|
||||||
// Change minimap char once the tile's secret has been uncovered.
|
|
||||||
|
|
||||||
constructor(id, portalTargetId, orientation) {
|
|
||||||
super({ id, portalTargetId, orientation });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.PI < 0 && ParsedCall && Orientation && Vector2i) {
|
|
||||||
("STFU Linda");
|
("STFU Linda");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,47 @@ export const PI_OVER_TWO = Math.PI / 2;
|
|||||||
* Enum Cardinal Direction (east north west south)
|
* Enum Cardinal Direction (east north west south)
|
||||||
* @constant @readonly @enum {number}
|
* @constant @readonly @enum {number}
|
||||||
*/
|
*/
|
||||||
export const Orientation = {
|
export class Orientation {
|
||||||
/** @constant @readonly @type {number} */
|
/** @constant @readonly @type {number} */
|
||||||
EAST: 0,
|
static EAST = 0;
|
||||||
/** @constant @readonly @type {number} */
|
/** @constant @readonly @type {number} */
|
||||||
SOUTH: 1,
|
static SOUTH = 1;
|
||||||
/** @constant @readonly @type {number} */
|
/** @constant @readonly @type {number} */
|
||||||
WEST: 2,
|
static WEST = 2;
|
||||||
/** @constant @readonly @type {number} */
|
/** @constant @readonly @type {number} */
|
||||||
NORTH: 3,
|
static NORTH = 3;
|
||||||
};
|
|
||||||
|
/**
|
||||||
|
* @param {string} str
|
||||||
|
* @returns {Orientation}
|
||||||
|
*/
|
||||||
|
static fromString(str) {
|
||||||
|
if (typeof str !== "string") {
|
||||||
|
console.error(
|
||||||
|
"Invalid data type when converting string to orientation. >>str<< is not a string be string.",
|
||||||
|
{ str },
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
str = str.toLowerCase();
|
||||||
|
if (str === "east") return Orientation.EAST;
|
||||||
|
if (str === "west") return Orientation.WEST;
|
||||||
|
if (str === "north") return Orientation.NORTH;
|
||||||
|
if (str === "south") return Orientation.SOUTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string|number} val
|
||||||
|
* @returns {Orientation}
|
||||||
|
*/
|
||||||
|
static normalize(val) {
|
||||||
|
if (typeof val === "string") {
|
||||||
|
return Orientation.fromString(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return val % 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum Relative Direction (forward, left, right, backwards)
|
* Enum Relative Direction (forward, left, right, backwards)
|
||||||
@@ -190,3 +221,7 @@ export class Vector2i {
|
|||||||
return `[${this.x} , ${this.y}]`;
|
return `[${this.x} , ${this.y}]`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const o = Orientation.fromString("south");
|
||||||
|
|
||||||
|
console.log(o);
|
||||||
|
|||||||
@@ -76,8 +76,6 @@ class MUDClient {
|
|||||||
// TODO Fix. Port should not be hardcoded
|
// TODO Fix. Port should not be hardcoded
|
||||||
const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, ":3000");
|
const wsUrl = `${protocol}//${window.location.host}`.replace(/:\d+$/, ":3000");
|
||||||
|
|
||||||
console.log(wsUrl);
|
|
||||||
|
|
||||||
this.updateStatus("Connecting...", "connecting");
|
this.updateStatus("Connecting...", "connecting");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -106,7 +104,7 @@ class MUDClient {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.websocket.onerror = (error) => {
|
this.websocket.onerror = (error) => {
|
||||||
console.log("Websocket error", error);
|
console.warn("Websocket error", error);
|
||||||
this.updateStatus("Connection Error", "error");
|
this.updateStatus("Connection Error", "error");
|
||||||
this.writeToOutput("Connection error occurred. Retrying...", { class: "error" });
|
this.writeToOutput("Connection error occurred. Retrying...", { class: "error" });
|
||||||
};
|
};
|
||||||
@@ -137,7 +135,7 @@ class MUDClient {
|
|||||||
* @param {...any} rest
|
* @param {...any} rest
|
||||||
*/
|
*/
|
||||||
send(messageType, ...args) {
|
send(messageType, ...args) {
|
||||||
console.log("sending", messageType, args);
|
console.debug("sending", messageType, args);
|
||||||
|
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
this.websocket.send(JSON.stringify([messageType]));
|
this.websocket.send(JSON.stringify([messageType]));
|
||||||
@@ -202,7 +200,6 @@ class MUDClient {
|
|||||||
// The quit command has its own message type
|
// The quit command has its own message type
|
||||||
let help = helpRegex.exec(inputText);
|
let help = helpRegex.exec(inputText);
|
||||||
if (help) {
|
if (help) {
|
||||||
console.log("here");
|
|
||||||
help[1] ? this.send(MessageType.HELP, help[1].trim()) : this.send(MessageType.HELP);
|
help[1] ? this.send(MessageType.HELP, help[1].trim()) : this.send(MessageType.HELP);
|
||||||
this.echo(inputText);
|
this.echo(inputText);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,573 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>ASCII Dungeon Generator</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
color: #00ff00;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
color: #00ff00;
|
|
||||||
text-shadow: 0 0 10px #00ff00;
|
|
||||||
}
|
|
||||||
.controls {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background-color: #333;
|
|
||||||
color: #00ff00;
|
|
||||||
border: 2px solid #00ff00;
|
|
||||||
padding: 10px 20px;
|
|
||||||
margin: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-family: "Courier New", monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background-color: #00ff00;
|
|
||||||
color: #1a1a1a;
|
|
||||||
box-shadow: 0 0 10px #00ff00;
|
|
||||||
}
|
|
||||||
.dungeon-display {
|
|
||||||
background-color: #000;
|
|
||||||
border: 2px solid #00ff00;
|
|
||||||
padding: 15px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1;
|
|
||||||
overflow-x: auto;
|
|
||||||
white-space: pre;
|
|
||||||
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
|
|
||||||
}
|
|
||||||
.settings {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.setting {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
input[type="range"] {
|
|
||||||
background-color: #333;
|
|
||||||
}
|
|
||||||
label {
|
|
||||||
color: #00ff00;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.legend {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 15px;
|
|
||||||
background-color: #222;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.legend h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #00ff00;
|
|
||||||
}
|
|
||||||
.legend-item {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>⚔️ ASCII DUNGEON GENERATOR ⚔️</h1>
|
|
||||||
|
|
||||||
<div class="settings">
|
|
||||||
<div class="setting">
|
|
||||||
<label for="width">Width:</label>
|
|
||||||
<input type="range" id="width" min="40" max="100" value="60" />
|
|
||||||
<span id="widthValue">60</span>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<label for="height">Height:</label>
|
|
||||||
<input type="range" id="height" min="30" max="60" value="40" />
|
|
||||||
<span id="heightValue">40</span>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<label for="roomCount">Rooms:</label>
|
|
||||||
<input type="range" id="roomCount" min="5" max="20" value="10" />
|
|
||||||
<span id="roomCountValue">10</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<button onclick="generateDungeon()">Generate New Dungeon</button>
|
|
||||||
<button onclick="downloadDungeon()">Download as Text</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dungeon-display" id="dungeonDisplay"></div>
|
|
||||||
|
|
||||||
<div class="legend">
|
|
||||||
<h3>Legend:</h3>
|
|
||||||
<div class="legend-item"><strong>#</strong> - Wall</div>
|
|
||||||
<div class="legend-item"><strong>.</strong> - Floor</div>
|
|
||||||
<div class="legend-item"><strong>+</strong> - Door</div>
|
|
||||||
<div class="legend-item"><strong>@</strong> - Player Start</div>
|
|
||||||
<div class="legend-item"><strong>$</strong> - Treasure</div>
|
|
||||||
<div class="legend-item"><strong>!</strong> - Monster</div>
|
|
||||||
<div class="legend-item"><strong>^</strong> - Trap</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
class Tile {
|
|
||||||
static FLOOR = new Tile(" ", true);
|
|
||||||
static RESERVED = new Tile(" ", true);
|
|
||||||
static PILLAR = new Tile("◯", false);
|
|
||||||
// static TRAP = new Tile("◡", true);
|
|
||||||
static TRAP = new Tile("☠", true);
|
|
||||||
static MONSTER = new Tile("!", true);
|
|
||||||
static WALL = new Tile("#", false);
|
|
||||||
static PLAYER_START = new Tile("@", true);
|
|
||||||
static DOOR = new Tile("░", true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} the utf-8 character that symbolizes this tile on the map
|
|
||||||
* @param {boolean} walkable can adventurers walk on this tile
|
|
||||||
*/
|
|
||||||
constructor(symbol, walkable = false) {
|
|
||||||
this.symbol = symbol;
|
|
||||||
this.walkable = walkable;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.symbol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DungeonGenerator {
|
|
||||||
constructor(width, height, roomCount) {
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
this.roomCount = roomCount;
|
|
||||||
this.grid = [];
|
|
||||||
this.rooms = [];
|
|
||||||
this.corridors = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
generate() {
|
|
||||||
this.initializeGrid();
|
|
||||||
this.generateRooms();
|
|
||||||
this.connectRooms();
|
|
||||||
this.addDoors();
|
|
||||||
this.addPillarsToBigRooms();
|
|
||||||
this.addFeatures();
|
|
||||||
this.checkAccessibility();
|
|
||||||
return this.gridToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeGrid() {
|
|
||||||
this.grid = Array(this.height)
|
|
||||||
.fill()
|
|
||||||
.map(() => Array(this.width).fill(Tile.WALL));
|
|
||||||
}
|
|
||||||
|
|
||||||
generateRooms() {
|
|
||||||
this.rooms = [];
|
|
||||||
const maxAttempts = this.roomCount * 10;
|
|
||||||
let attempts = 0;
|
|
||||||
|
|
||||||
while (this.rooms.length < this.roomCount && attempts < maxAttempts) {
|
|
||||||
const room = this.generateRoom();
|
|
||||||
if (room && !this.roomOverlaps(room)) {
|
|
||||||
this.rooms.push(room);
|
|
||||||
this.carveRoom(room);
|
|
||||||
}
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generateRoom() {
|
|
||||||
const minSize = 4;
|
|
||||||
const maxSize = Math.min(12, Math.floor(Math.min(this.width, this.height) / 4));
|
|
||||||
|
|
||||||
const width = this.random(minSize, maxSize);
|
|
||||||
const height = this.random(minSize, maxSize);
|
|
||||||
const x = this.random(1, this.width - width - 1);
|
|
||||||
const y = this.random(1, this.height - height - 1);
|
|
||||||
|
|
||||||
return { x, y, width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
roomOverlaps(newRoom) {
|
|
||||||
return this.rooms.some(
|
|
||||||
(room) =>
|
|
||||||
newRoom.x < room.x + room.width + 2 &&
|
|
||||||
newRoom.x + newRoom.width + 2 > room.x &&
|
|
||||||
newRoom.y < room.y + room.height + 2 &&
|
|
||||||
newRoom.y + newRoom.height + 2 > room.y,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
carveRoom(room) {
|
|
||||||
for (let y = room.y; y < room.y + room.height; y++) {
|
|
||||||
for (let x = room.x; x < room.x + room.width; x++) {
|
|
||||||
this.grid[y][x] = Tile.FLOOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectRooms() {
|
|
||||||
if (this.rooms.length < 2) return;
|
|
||||||
|
|
||||||
// Connect each room to at least one other room
|
|
||||||
for (let i = 1; i < this.rooms.length; i++) {
|
|
||||||
const roomA = this.rooms[i - 1];
|
|
||||||
const roomB = this.rooms[i];
|
|
||||||
this.createCorridor(roomA, roomB);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add some extra connections for more interesting layouts
|
|
||||||
const extraConnections = Math.floor(this.rooms.length / 3);
|
|
||||||
for (let i = 0; i < extraConnections; i++) {
|
|
||||||
const roomA = this.rooms[this.random(0, this.rooms.length - 1)];
|
|
||||||
const roomB = this.rooms[this.random(0, this.rooms.length - 1)];
|
|
||||||
if (roomA !== roomB) {
|
|
||||||
this.createCorridor(roomA, roomB);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
createCorridor(roomA, roomB) {
|
|
||||||
const startX = Math.floor(roomA.x + roomA.width / 2);
|
|
||||||
const startY = Math.floor(roomA.y + roomA.height / 2);
|
|
||||||
const endX = Math.floor(roomB.x + roomB.width / 2);
|
|
||||||
const endY = Math.floor(roomB.y + roomB.height / 2);
|
|
||||||
|
|
||||||
// Create L-shaped corridor
|
|
||||||
if (Math.random() < 0.5) {
|
|
||||||
// Horizontal first, then vertical
|
|
||||||
this.carveLine(startX, startY, endX, startY);
|
|
||||||
this.carveLine(endX, startY, endX, endY);
|
|
||||||
} else {
|
|
||||||
// Vertical first, then horizontal
|
|
||||||
this.carveLine(startX, startY, startX, endY);
|
|
||||||
this.carveLine(startX, endY, endX, endY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
carveLine(x1, y1, x2, y2) {
|
|
||||||
const dx = Math.sign(x2 - x1);
|
|
||||||
const dy = Math.sign(y2 - y1);
|
|
||||||
|
|
||||||
let x = x1;
|
|
||||||
let y = y1;
|
|
||||||
|
|
||||||
while (x !== x2 || y !== y2) {
|
|
||||||
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
|
||||||
this.grid[y][x] = Tile.FLOOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x !== x2) x += dx;
|
|
||||||
if (y !== y2 && x === x2) y += dy;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure endpoint is carved
|
|
||||||
if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) {
|
|
||||||
this.grid[y2][x2] = Tile.FLOOR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addDoors() {
|
|
||||||
this.rooms.forEach((room) => {
|
|
||||||
const doors = [];
|
|
||||||
|
|
||||||
// Check each wall of the room for potential doors
|
|
||||||
for (let x = room.x; x < room.x + room.width; x++) {
|
|
||||||
// Top wall
|
|
||||||
if (
|
|
||||||
room.y > 0 &&
|
|
||||||
this.grid[room.y - 1][x] === Tile.FLOOR &&
|
|
||||||
this.grid[room.y][x] === Tile.FLOOR
|
|
||||||
) {
|
|
||||||
doors.push({ x, y: room.y });
|
|
||||||
}
|
|
||||||
// Bottom wall
|
|
||||||
if (
|
|
||||||
room.y + room.height < this.height &&
|
|
||||||
this.grid[room.y + room.height][x] === Tile.FLOOR &&
|
|
||||||
this.grid[room.y + room.height - 1][x] === Tile.FLOOR
|
|
||||||
) {
|
|
||||||
doors.push({ x, y: room.y + room.height - 1 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let y = room.y; y < room.y + room.height; y++) {
|
|
||||||
// Left wall
|
|
||||||
if (
|
|
||||||
room.x > 0 &&
|
|
||||||
this.grid[y][room.x - 1] === Tile.FLOOR &&
|
|
||||||
this.grid[y][room.x] === Tile.FLOOR
|
|
||||||
) {
|
|
||||||
doors.push({ x: room.x, y });
|
|
||||||
}
|
|
||||||
// Right wall
|
|
||||||
if (
|
|
||||||
room.x + room.width < this.width &&
|
|
||||||
this.grid[y][room.x + room.width] === Tile.FLOOR &&
|
|
||||||
this.grid[y][room.x + room.width - 1] === Tile.FLOOR
|
|
||||||
) {
|
|
||||||
doors.push({ x: room.x + room.width - 1, y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a few doors randomly
|
|
||||||
doors.forEach((door) => {
|
|
||||||
if (Math.random() < 0.3) {
|
|
||||||
this.grid[door.y][door.x] = Tile.DOOR;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
addPillarsToBigRooms() {
|
|
||||||
const walkabilityCache = [];
|
|
||||||
let i = 0;
|
|
||||||
for (let y = 1; y < this.height - 1; y++) {
|
|
||||||
//
|
|
||||||
for (let x = 1; x < this.width - 1; x++) {
|
|
||||||
i++;
|
|
||||||
|
|
||||||
const cell = this.grid[y][x];
|
|
||||||
|
|
||||||
if (!cell) {
|
|
||||||
console.log("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.grid[y][x].walkable) {
|
|
||||||
walkabilityCache.push([i, x, y]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shuffle = (arr) => {
|
|
||||||
for (let i = arr.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1)); // random index 0..i
|
|
||||||
[arr[i], arr[j]] = [arr[j], arr[i]]; // swap
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
};
|
|
||||||
|
|
||||||
shuffle(walkabilityCache);
|
|
||||||
|
|
||||||
for (let [i, x, y] of walkabilityCache) {
|
|
||||||
const walkable = (offsetX, offsetY) => {
|
|
||||||
const c = this.grid[y + offsetY][x + offsetX];
|
|
||||||
return c.walkable;
|
|
||||||
};
|
|
||||||
|
|
||||||
const surroundingFloorCount =
|
|
||||||
0 +
|
|
||||||
// top row ------------|-----------
|
|
||||||
walkable(-1, -1) + // | north west
|
|
||||||
walkable(+0, -1) + // | north
|
|
||||||
walkable(+1, -1) + // | north east
|
|
||||||
// middle row ---------|-----------
|
|
||||||
walkable(-1, +0) + // | west
|
|
||||||
// | self
|
|
||||||
walkable(+1, +0) + // | east
|
|
||||||
// bottom row ---------|-----------
|
|
||||||
walkable(-1, +1) + // | south west
|
|
||||||
walkable(+0, +1) + // | south
|
|
||||||
walkable(+1, +1); // | south east
|
|
||||||
// ----------------------------|-----------
|
|
||||||
|
|
||||||
if (surroundingFloorCount === 8) {
|
|
||||||
this.grid[y][x] = Tile.PILLAR;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (surroundingFloorCount >= 7) {
|
|
||||||
this.grid[y][x] = Tile.WALL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that all rooms are accessibly from the start
|
|
||||||
checkAccessibility() {
|
|
||||||
let playerStartIdx;
|
|
||||||
let walkableTileCount = 0;
|
|
||||||
const walkabilityCache = [];
|
|
||||||
|
|
||||||
// Create a flat linear version of the grid, consisting
|
|
||||||
// only of booleans: true of passable, false if obstacle.
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
|
||||||
const cell = this.grid[y][x];
|
|
||||||
const isObstacle = cell === Tile.WALL || cell === Tile.PILLAR;
|
|
||||||
|
|
||||||
if (cell === Tile.PLAYER_START) {
|
|
||||||
playerStartIdx = walkabilityCache.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isObstacle) {
|
|
||||||
walkableTileCount++;
|
|
||||||
walkabilityCache.push(true);
|
|
||||||
} else {
|
|
||||||
walkabilityCache.push(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toXy = (idx) => {
|
|
||||||
return [idx % this.width, Math.floor(idx / this.width)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const stack = [playerStartIdx];
|
|
||||||
|
|
||||||
/** @type {Set} */
|
|
||||||
const visited = new Set();
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const idx = stack.pop();
|
|
||||||
|
|
||||||
if (!walkabilityCache[idx]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visited.has(idx)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
visited.add(idx);
|
|
||||||
|
|
||||||
// Add neighbors
|
|
||||||
const [x, y] = toXy(idx);
|
|
||||||
const [minX, minY] = [1, 1];
|
|
||||||
const maxX = this.width - 2;
|
|
||||||
const maxY = this.height - 2;
|
|
||||||
|
|
||||||
if (y >= minY) stack.push(idx - this.width); // up
|
|
||||||
if (y <= maxY) stack.push(idx + this.width); // down
|
|
||||||
if (x >= minX) stack.push(idx - 1); // left
|
|
||||||
if (x <= maxX) stack.push(idx + 1); // right
|
|
||||||
}
|
|
||||||
if (visited.size !== walkableTileCount) {
|
|
||||||
console.log(
|
|
||||||
"unpassable! There are %d floor tiles, but the player can only visit %d of them",
|
|
||||||
walkableTileCount,
|
|
||||||
visited.size,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
//
|
|
||||||
addFeatures() {
|
|
||||||
const floorTiles = [];
|
|
||||||
for (let y = 0; y < this.height; y++) {
|
|
||||||
for (let x = 0; x < this.width; x++) {
|
|
||||||
if (this.grid[y][x] === Tile.FLOOR) {
|
|
||||||
floorTiles.push({ x, y });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (floorTiles.length === 0) return;
|
|
||||||
|
|
||||||
// Add player start
|
|
||||||
const playerStart = floorTiles[this.random(0, floorTiles.length - 1)];
|
|
||||||
this.grid[playerStart.y][playerStart.x] = "@";
|
|
||||||
|
|
||||||
// Add treasures
|
|
||||||
const treasureCount = Math.min(3, Math.floor(this.rooms.length / 2));
|
|
||||||
for (let i = 0; i < treasureCount; i++) {
|
|
||||||
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
|
||||||
if (this.grid[pos.y][pos.x] === Tile.FLOOR) {
|
|
||||||
this.grid[pos.y][pos.x] = "$";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add monsters
|
|
||||||
const monsterCount = Math.min(5, this.rooms.length);
|
|
||||||
for (let i = 0; i < monsterCount; i++) {
|
|
||||||
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
|
||||||
if (this.grid[pos.y][pos.x] === Tile.FLOOR) {
|
|
||||||
this.grid[pos.y][pos.x] = Tile.MONSTER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add traps
|
|
||||||
const trapCount = Math.floor(floorTiles.length / 30);
|
|
||||||
for (let i = 0; i < trapCount; i++) {
|
|
||||||
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
|
||||||
if (this.grid[pos.y][pos.x] === Tile.FLOOR) {
|
|
||||||
this.grid[pos.y][pos.x] = Tile.TRAP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gridToString() {
|
|
||||||
return this.grid.map((row) => row.join("")).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
random(min, max) {
|
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentDungeon = "";
|
|
||||||
|
|
||||||
function generateDungeon() {
|
|
||||||
const width = parseInt(document.getElementById("width").value);
|
|
||||||
const height = parseInt(document.getElementById("height").value);
|
|
||||||
const roomCount = parseInt(document.getElementById("roomCount").value);
|
|
||||||
|
|
||||||
const generator = new DungeonGenerator(width, height, roomCount);
|
|
||||||
currentDungeon = generator.generate();
|
|
||||||
|
|
||||||
document.getElementById("dungeonDisplay").textContent = currentDungeon;
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadDungeon() {
|
|
||||||
if (!currentDungeon) {
|
|
||||||
generateDungeon();
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([currentDungeon], { type: "text/plain" });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.href = url;
|
|
||||||
a.download = "dungeon_map.txt";
|
|
||||||
document.body.appendChild(a);
|
|
||||||
a.click();
|
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update slider value displays
|
|
||||||
document.getElementById("width").addEventListener("input", function () {
|
|
||||||
document.getElementById("widthValue").textContent = this.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("height").addEventListener("input", function () {
|
|
||||||
document.getElementById("heightValue").textContent = this.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("roomCount").addEventListener("input", function () {
|
|
||||||
document.getElementById("roomCountValue").textContent = this.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate initial dungeon
|
|
||||||
generateDungeon();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
133
frontend/dungeon_studio.html
Executable file
133
frontend/dungeon_studio.html
Executable file
@@ -0,0 +1,133 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ASCII Dungeon Generator</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
background-color: #9a9;
|
||||||
|
color: #aba;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
color: #00ff00;
|
||||||
|
text-shadow: 0 0 10px #00ff00;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #333;
|
||||||
|
color: #00ff00;
|
||||||
|
border: 2px solid #00ff00;
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
background-color: #00ff00;
|
||||||
|
color: #1a1a1a;
|
||||||
|
box-shadow: 0 0 10px #00ff00;
|
||||||
|
}
|
||||||
|
.dungeon-display {
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
background-color: #000;
|
||||||
|
border: 2px solid #777;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 17px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
|
||||||
|
}
|
||||||
|
.settings {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.setting {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
input[type="range"] {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
color: #00ff00;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.legend {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #222;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.legend h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #1f1;
|
||||||
|
}
|
||||||
|
.legend-item {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>⚔️ ASCII DUNGEON GENERATOR ⚔️</h1>
|
||||||
|
|
||||||
|
<div class="settings">
|
||||||
|
<div class="setting">
|
||||||
|
<label for="width">Width:</label>
|
||||||
|
<input type="range" id="width" min="40" max="100" value="69" />
|
||||||
|
<span id="widthValue">69</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<label for="height">Height:</label>
|
||||||
|
<input type="range" id="height" min="30" max="60" value="42" />
|
||||||
|
<span id="heightValue">42</span>
|
||||||
|
</div>
|
||||||
|
<div class="setting">
|
||||||
|
<label for="roomCount">Rooms:</label>
|
||||||
|
<input type="range" id="roomCount" min="5" max="20" value="18" />
|
||||||
|
<span id="roomCountValue">18</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button onclick="generateDungeon()">Generate New Dungeon</button>
|
||||||
|
<button onclick="downloadDungeon()">Download as Text</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dungeon-display" id="dungeonDisplay" contenteditable="true"></div>
|
||||||
|
|
||||||
|
<div class="legend">
|
||||||
|
<h3>Legend:</h3>
|
||||||
|
<div class="legend-item"><strong>#</strong> - Wall</div>
|
||||||
|
<div class="legend-item"><strong>.</strong> - Floor</div>
|
||||||
|
<div class="legend-item"><strong>+</strong> - Door</div>
|
||||||
|
<div class="legend-item"><strong>@</strong> - Player Start</div>
|
||||||
|
<div class="legend-item"><strong>$</strong> - Treasure</div>
|
||||||
|
<div class="legend-item"><strong>!</strong> - Monster</div>
|
||||||
|
<div class="legend-item"><strong>^</strong> - Trap</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="./dungeon_studio.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
446
frontend/dungeon_studio.js
Executable file
446
frontend/dungeon_studio.js
Executable file
@@ -0,0 +1,446 @@
|
|||||||
|
import { CharType, TileMap } from "./ascii_tile_map";
|
||||||
|
import { EncounterTile, FloorTile, PlayerStartTile, TrapTile, LootTile, WallTile } from "./ascii_tile_types";
|
||||||
|
import { Orientation } from "./ascii_types";
|
||||||
|
|
||||||
|
class DungeonGenerator {
|
||||||
|
constructor(width, height, roomCount) {
|
||||||
|
this.roomCount = roomCount;
|
||||||
|
this.rooms = [];
|
||||||
|
this.corridors = [];
|
||||||
|
|
||||||
|
// 2d array of pure wall tiles
|
||||||
|
const tiles = new Array(height).fill().map(() => Array(width).fill(new WallTile()));
|
||||||
|
|
||||||
|
this.map = new TileMap(tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this.map.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this.map.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
generate() {
|
||||||
|
this.generateRooms();
|
||||||
|
this.connectRooms();
|
||||||
|
this.trimMap();
|
||||||
|
this.addPillarsToBigRooms();
|
||||||
|
this.addFeatures();
|
||||||
|
this.addPlayerStart();
|
||||||
|
this.addPortals();
|
||||||
|
return this.map.toString(CharType.MINIMAP_REVEALED);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateRooms() {
|
||||||
|
this.rooms = [];
|
||||||
|
const maxAttempts = this.roomCount * 10;
|
||||||
|
let attempts = 0;
|
||||||
|
|
||||||
|
while (this.rooms.length < this.roomCount && attempts < maxAttempts) {
|
||||||
|
const room = this.generateRoom();
|
||||||
|
if (room && !this.roomOverlaps(room)) {
|
||||||
|
this.rooms.push(room);
|
||||||
|
this.carveRoom(room);
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateRoom() {
|
||||||
|
const minSize = 4;
|
||||||
|
const maxSize = Math.min(12, Math.floor(Math.min(this.width, this.height) / 4));
|
||||||
|
|
||||||
|
const width = this.random(minSize, maxSize);
|
||||||
|
const height = this.random(minSize, maxSize);
|
||||||
|
const x = this.random(1, this.width - width - 1);
|
||||||
|
const y = this.random(1, this.height - height - 1);
|
||||||
|
|
||||||
|
return { x, y, width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
roomOverlaps(newRoom) {
|
||||||
|
return this.rooms.some(
|
||||||
|
(room) =>
|
||||||
|
newRoom.x < room.x + room.width + 2 &&
|
||||||
|
newRoom.x + newRoom.width + 2 > room.x &&
|
||||||
|
newRoom.y < room.y + room.height + 2 &&
|
||||||
|
newRoom.y + newRoom.height + 2 > room.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
carveRoom(room) {
|
||||||
|
for (let y = room.y; y < room.y + room.height; y++) {
|
||||||
|
for (let x = room.x; x < room.x + room.width; x++) {
|
||||||
|
this.map.tiles[y][x] = new FloorTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectRooms() {
|
||||||
|
if (this.rooms.length < 2) return;
|
||||||
|
|
||||||
|
// Connect each room to at least one other room
|
||||||
|
for (let i = 1; i < this.rooms.length >> 1; i++) {
|
||||||
|
const roomA = this.rooms[i - 1];
|
||||||
|
const roomB = this.rooms[i];
|
||||||
|
this.createCorridor(roomA, roomB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some extra connections for more interesting layouts
|
||||||
|
const extraConnections = Math.floor(this.rooms.length / 3);
|
||||||
|
for (let i = 0; i < extraConnections; i++) {
|
||||||
|
const roomA = this.rooms[this.random(0, this.rooms.length - 1)];
|
||||||
|
const roomB = this.rooms[this.random(0, this.rooms.length - 1)];
|
||||||
|
if (roomA !== roomB) {
|
||||||
|
this.createCorridor(roomA, roomB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove unnecessary walls that frame the rooms
|
||||||
|
// The dungeon should only be framed by a single
|
||||||
|
// layer of walls
|
||||||
|
trimMap() {
|
||||||
|
let dungeonStartY = undefined;
|
||||||
|
let dungeonEndY = 0;
|
||||||
|
|
||||||
|
let dungeonStartX = this.width; // among all rows, when did we first see a non-wall tile on the west-side of the map?
|
||||||
|
let dungeonEndX = 0; // among all rows, when did we last see a non-wall tile on the east-side of the map?
|
||||||
|
|
||||||
|
for (let y = 0; y < this.height; y++) {
|
||||||
|
//
|
||||||
|
let firstNonWallX = undefined; // x-index of the FIRST (westmost) non-wall tile that we encountered on this row
|
||||||
|
let lastNonWallX = undefined; // x-index of the LAST (eastmost) non-wall tile that we encountered on this row
|
||||||
|
|
||||||
|
for (let x = 0; x < this.width; x++) {
|
||||||
|
const isWall = this.map.get(x, y) instanceof WallTile;
|
||||||
|
|
||||||
|
if (isWall) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstNonWallX === undefined) {
|
||||||
|
firstNonWallX = x;
|
||||||
|
}
|
||||||
|
lastNonWallX = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlyWalls = lastNonWallX === undefined;
|
||||||
|
if (onlyWalls) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// X-axis bookkeeping
|
||||||
|
if (dungeonStartX > 0 && lastNonWallX < this.width) {
|
||||||
|
dungeonStartX = Math.min(dungeonStartX, firstNonWallX);
|
||||||
|
dungeonEndX = Math.max(dungeonEndX, lastNonWallX);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Y-Axis bookkeeping
|
||||||
|
if (dungeonStartY === undefined) {
|
||||||
|
dungeonStartY = y;
|
||||||
|
}
|
||||||
|
dungeonEndY = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = dungeonEndX - dungeonStartX + 3;
|
||||||
|
|
||||||
|
const newTiles = [];
|
||||||
|
|
||||||
|
// First row is all walls
|
||||||
|
newTiles.push(new Array(newWidth).fill(new WallTile()));
|
||||||
|
|
||||||
|
// Populate the new grid
|
||||||
|
for (let y = dungeonStartY; y <= dungeonEndY; y++) {
|
||||||
|
const row = [];
|
||||||
|
|
||||||
|
row.push(new WallTile()); // Initial wall tile on this row
|
||||||
|
for (let x = dungeonStartX; x <= dungeonEndX; x++) {
|
||||||
|
/**/
|
||||||
|
const tile = this.map.get(x, y);
|
||||||
|
row.push(tile);
|
||||||
|
}
|
||||||
|
row.push(new WallTile()); // Final wall tile on this row
|
||||||
|
newTiles.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final row is all walls
|
||||||
|
newTiles.push(new Array(newWidth).fill(new WallTile()));
|
||||||
|
|
||||||
|
this.map = new TileMap(newTiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCorridor(roomA, roomB) {
|
||||||
|
const startX = Math.floor(roomA.x + roomA.width / 2);
|
||||||
|
const startY = Math.floor(roomA.y + roomA.height / 2);
|
||||||
|
const endX = Math.floor(roomB.x + roomB.width / 2);
|
||||||
|
const endY = Math.floor(roomB.y + roomB.height / 2);
|
||||||
|
|
||||||
|
// Create L-shaped corridor
|
||||||
|
if (Math.random() < 0.5) {
|
||||||
|
// Horizontal first, then vertical
|
||||||
|
this.carveLine(startX, startY, endX, startY);
|
||||||
|
this.carveLine(endX, startY, endX, endY);
|
||||||
|
} else {
|
||||||
|
// Vertical first, then horizontal
|
||||||
|
this.carveLine(startX, startY, startX, endY);
|
||||||
|
this.carveLine(startX, endY, endX, endY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
carveLine(x1, y1, x2, y2) {
|
||||||
|
const dx = Math.sign(x2 - x1);
|
||||||
|
const dy = Math.sign(y2 - y1);
|
||||||
|
|
||||||
|
let x = x1;
|
||||||
|
let y = y1;
|
||||||
|
|
||||||
|
while (x !== x2 || y !== y2) {
|
||||||
|
if (x >= 0 && x < this.width && y >= 0 && y < this.height) {
|
||||||
|
this.map.tiles[y][x] = new FloorTile();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x !== x2) x += dx;
|
||||||
|
if (y !== y2 && x === x2) y += dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure endpoint is carved
|
||||||
|
if (x2 >= 0 && x2 < this.width && y2 >= 0 && y2 < this.height) {
|
||||||
|
this.map.tiles[y2][x2] = new FloorTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addPillarsToBigRooms() {
|
||||||
|
const walkabilityCache = [];
|
||||||
|
for (let y = 1; y < this.height - 1; y++) {
|
||||||
|
//
|
||||||
|
for (let x = 1; x < this.width - 1; x++) {
|
||||||
|
const cell = this.map.get(x, y);
|
||||||
|
|
||||||
|
if (!cell) {
|
||||||
|
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.map.isTraversable(x, y)) {
|
||||||
|
walkabilityCache.push([x, y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shuffle = (arr) => {
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1)); // random index 0..i
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]]; // swap
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
shuffle(walkabilityCache);
|
||||||
|
|
||||||
|
for (let [x, y] of walkabilityCache) {
|
||||||
|
//
|
||||||
|
const walkable = (offsetX, offsetY) => this.map.isTraversable(x + offsetX, y + offsetY);
|
||||||
|
|
||||||
|
const surroundingFloorCount =
|
||||||
|
0 +
|
||||||
|
// top row ------------|-----------
|
||||||
|
walkable(-1, -1) + // | north west
|
||||||
|
walkable(+0, -1) + // | north
|
||||||
|
walkable(+1, -1) + // | north east
|
||||||
|
// middle row ---------|-----------
|
||||||
|
walkable(-1, +0) + // | west
|
||||||
|
// | self
|
||||||
|
walkable(+1, +0) + // | east
|
||||||
|
// bottom row ---------|-----------
|
||||||
|
walkable(-1, +1) + // | south west
|
||||||
|
walkable(+0, +1) + // | south
|
||||||
|
walkable(+1, +1); // | south east
|
||||||
|
// ----------------------------|-----------
|
||||||
|
|
||||||
|
if (surroundingFloorCount >= 7) {
|
||||||
|
// MAGIC NUMBER 7
|
||||||
|
this.map.tiles[y][x] = new WallTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addPlayerStart() {
|
||||||
|
const walkabilityCache = [];
|
||||||
|
for (let y = 1; y < this.height - 1; y++) {
|
||||||
|
//
|
||||||
|
for (let x = 1; x < this.width - 1; x++) {
|
||||||
|
const cell = this.map.get(x, y);
|
||||||
|
|
||||||
|
if (!cell) {
|
||||||
|
console.warn("out of bounds [%d, %d] (%s)", x, y, typeof cell);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.map.isTraversable(x, y)) {
|
||||||
|
walkabilityCache.push([x, y]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = this.random(0, walkabilityCache.length - 1);
|
||||||
|
const [x, y] = walkabilityCache[idx];
|
||||||
|
|
||||||
|
const walkable = (offsetX, offsetY) => this.map.isTraversable(x + offsetX, y + offsetY);
|
||||||
|
|
||||||
|
//
|
||||||
|
// When spawning in, which direction should the player be oriented?
|
||||||
|
//
|
||||||
|
const directions = [];
|
||||||
|
if (walkable(+1, +0)) directions.push(Orientation.EAST);
|
||||||
|
if (walkable(+0, +1)) directions.push(Orientation.NORTH);
|
||||||
|
if (walkable(-1, +0)) directions.push(Orientation.WEST);
|
||||||
|
if (walkable(+0, -1)) directions.push(Orientation.SOUTH);
|
||||||
|
|
||||||
|
const dirIdx = this.random(0, directions.length - 1);
|
||||||
|
|
||||||
|
this.map.tiles[y][x] = new PlayerStartTile(directions[dirIdx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add portals to isolated areas
|
||||||
|
addPortals() {
|
||||||
|
let traversableTileCount = this.map.getTraversableTileCount();
|
||||||
|
|
||||||
|
const result = this.map.getAllTraversableTilesConnectedTo(/** TODO PlayerPos */);
|
||||||
|
|
||||||
|
if (result.size === traversableTileCount) {
|
||||||
|
// There are no isolated areas, return
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// _____ ___ ____ ___
|
||||||
|
// |_ _/ _ \| _ \ / _ \
|
||||||
|
// | || | | | | | | | | |
|
||||||
|
// | || |_| | |_| | |_| |
|
||||||
|
// |_| \___/|____/ \___/
|
||||||
|
//-------------------------------------
|
||||||
|
// Connect isolated rooms via portals
|
||||||
|
//-------------------------------------
|
||||||
|
//
|
||||||
|
// LET Area0 = getAllTilesConnectedTo(playerStartTile)
|
||||||
|
// LET Areas = Array containing one item so far: Area0
|
||||||
|
// FOR EACH tile in this.map
|
||||||
|
// IF tile not painted
|
||||||
|
// LET newArea = getAllTilesConnectedTo(tile)
|
||||||
|
// PUSH newArea ONTO Areas
|
||||||
|
//
|
||||||
|
// FOR EACH area IN Areas
|
||||||
|
// LET index = IndexOf(Areas, area)
|
||||||
|
// LET next = index + 1 mod LENGTH(Areas)
|
||||||
|
// entryPos = findValidPortalEntryPositionInArea(area)
|
||||||
|
// exitPos = findValidPortalExitPositionInArea(area)
|
||||||
|
//
|
||||||
|
// this.map[entryPos.y, entryPos.x] = new PortalEntryTile(index)
|
||||||
|
// this.map[exitPos.y, exitPos.x] = new PortalExitTile(next)
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Start pointing it (another color)
|
||||||
|
//
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"unpassable! There are %d floor tiles, but the player can only visit %d of them",
|
||||||
|
traversableTileCount,
|
||||||
|
result.size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
addFeatures() {
|
||||||
|
const floorTiles = [];
|
||||||
|
for (let y = 0; y < this.height; y++) {
|
||||||
|
for (let x = 0; x < this.width; x++) {
|
||||||
|
if (this.map.get(x, y) instanceof FloorTile) {
|
||||||
|
floorTiles.push({ x, y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (floorTiles.length === 0) return;
|
||||||
|
|
||||||
|
// Add loot
|
||||||
|
const lootCount = Math.min(3, Math.floor(this.rooms.length / 2));
|
||||||
|
for (let i = 0; i < lootCount; i++) {
|
||||||
|
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
||||||
|
if (this.map.tiles[pos.y][pos.x] instanceof FloorTile) {
|
||||||
|
this.map.tiles[pos.y][pos.x] = new LootTile(undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add monsters
|
||||||
|
const monsterCount = Math.min(5, this.rooms.length);
|
||||||
|
for (let i = 0; i < monsterCount; i++) {
|
||||||
|
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
||||||
|
if (this.map.tiles[pos.y][pos.x] instanceof FloorTile) {
|
||||||
|
this.map.tiles[pos.y][pos.x] = new EncounterTile(pos.x, pos.y, undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add traps
|
||||||
|
const trapCount = Math.floor(floorTiles.length / 30);
|
||||||
|
for (let i = 0; i < trapCount; i++) {
|
||||||
|
const pos = floorTiles[this.random(0, floorTiles.length - 1)];
|
||||||
|
if (this.map.tiles[pos.y][pos.x] instanceof FloorTile) {
|
||||||
|
this.map.tiles[pos.y][pos.x] = new TrapTile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
random(min, max) {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentDungeon = "";
|
||||||
|
|
||||||
|
window.generateDungeon = () => {
|
||||||
|
const width = parseInt(document.getElementById("width").value);
|
||||||
|
const height = parseInt(document.getElementById("height").value);
|
||||||
|
const roomCount = parseInt(document.getElementById("roomCount").value);
|
||||||
|
|
||||||
|
const generator = new DungeonGenerator(width, height, roomCount);
|
||||||
|
currentDungeon = generator.generate();
|
||||||
|
|
||||||
|
document.getElementById("dungeonDisplay").textContent = currentDungeon;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.downloadDungeon = () => {
|
||||||
|
if (!currentDungeon) {
|
||||||
|
window.generateDungeon();
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([currentDungeon], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "dungeon_map.txt";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById("width").addEventListener("input", function () {
|
||||||
|
document.getElementById("widthValue").textContent = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("height").addEventListener("input", function () {
|
||||||
|
document.getElementById("heightValue").textContent = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("roomCount").addEventListener("input", function () {
|
||||||
|
document.getElementById("roomCountValue").textContent = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate initial dungeon
|
||||||
|
window.generateDungeon();
|
||||||
BIN
frontend/skelebones.png
Normal file
BIN
frontend/skelebones.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@@ -202,14 +202,14 @@ class MudServer {
|
|||||||
//
|
//
|
||||||
// Handle system messages
|
// Handle system messages
|
||||||
if (msgObj.isSysMessage()) {
|
if (msgObj.isSysMessage()) {
|
||||||
console.log("SYS message", msgObj);
|
console.debug("SYS message", msgObj);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Handle debug messages
|
// Handle debug messages
|
||||||
if (msgObj.isDebug()) {
|
if (msgObj.isDebug()) {
|
||||||
console.log("DBG message", msgObj);
|
console.debug("DBG message", msgObj);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
test.js
17
test.js
@@ -0,0 +1,17 @@
|
|||||||
|
class Nugga {
|
||||||
|
mufassa = 22;
|
||||||
|
constructor() {
|
||||||
|
this.fjæsing = 22;
|
||||||
|
console.debug(Object.prototype.hasOwnProperty.call(this, "fjæsing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
diller(snaps = this.fjæsing) {
|
||||||
|
console.log(snaps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dugga extends Nugga {}
|
||||||
|
|
||||||
|
const n = new Dugga();
|
||||||
|
|
||||||
|
console.log(n, n.diller(), n instanceof Dugga);
|
||||||
|
|||||||
@@ -95,6 +95,3 @@ export class Xorshift32 {
|
|||||||
return num + greaterThanOrEqual;
|
return num + greaterThanOrEqual;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const rng = new Xorshift32();
|
|
||||||
console.log(rng.get());
|
|
||||||
|
|||||||
42
utils/shallowCopy.js
Executable file
42
utils/shallowCopy.js
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Shallow copy any JS value if it makes sense.
|
||||||
|
* @param {*} value
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
export default function shallowCopy(value) {
|
||||||
|
if (value === null || typeof value !== "object") {
|
||||||
|
// primitives, functions, symbols
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return new Date(value.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return new Map(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Set) {
|
||||||
|
return new Set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain objects
|
||||||
|
if (Object.getPrototypeOf(value) === Object.prototype) {
|
||||||
|
return Object.assign({}, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value?.clone === "function") {
|
||||||
|
return value.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: clone prototype + own props
|
||||||
|
return Object.create(
|
||||||
|
Object.getPrototypeOf(value), //
|
||||||
|
Object.getOwnPropertyDescriptors(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/** A call represents the name of a function as well as the arguments passed to it */
|
/** A call represents the name of a function as well as the arguments passed to it */
|
||||||
export class ParsedCall {
|
export class TileOptions {
|
||||||
/** @type {string} Name of the function */ name;
|
/** @type {string} Name of the function */ name;
|
||||||
/** @type {ParsedArg[]} Args passed to function */ args;
|
/** @type {TileArgs[]} Args passed to function */ args;
|
||||||
|
|
||||||
constructor(name, args) {
|
constructor(name, args) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -14,7 +14,7 @@ export class ParsedCall {
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {number?} position
|
* @param {number?} position
|
||||||
*
|
*
|
||||||
* @returns {ParsedArg|null}
|
* @returns {TileArgs|null}
|
||||||
*/
|
*/
|
||||||
getArg(name, position) {
|
getArg(name, position) {
|
||||||
for (let idx in this.args) {
|
for (let idx in this.args) {
|
||||||
@@ -32,10 +32,28 @@ export class ParsedCall {
|
|||||||
const arg = this.getArg(name, position);
|
const arg = this.getArg(name, position);
|
||||||
return arg ? arg.value : fallbackValue;
|
return arg ? arg.value : fallbackValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} includePositionals Should the result object include numeric entries for the positional arguments?
|
||||||
|
* @returns {object} object where the keys are the names of the named args, and the values are the values of those args.
|
||||||
|
*/
|
||||||
|
getNamedValues(includePositionals = false) {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for (const arg of this.args) {
|
||||||
|
const key = arg.key;
|
||||||
|
|
||||||
|
if (includePositionals || typeof key === "string") {
|
||||||
|
result[key] = arg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An argument passed to a function. Can be positional or named */
|
/** An argument passed to a function. Can be positional or named */
|
||||||
export class ParsedArg {
|
export class TileArgs {
|
||||||
/** @type {string|number} */ key;
|
/** @type {string|number} */ key;
|
||||||
/** @type {string|number|boolean|null|undefined} */ value;
|
/** @type {string|number|boolean|null|undefined} */ value;
|
||||||
constructor(key, value) {
|
constructor(key, value) {
|
||||||
@@ -45,11 +63,11 @@ export class ParsedArg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a string that includes a number of function calls separated by ";" semicolons
|
* Parse a string of options that looks like function calls separated by ";" semicolons
|
||||||
*
|
*
|
||||||
* @param {string} input
|
* @param {string} input
|
||||||
*
|
*
|
||||||
* @returns {ParsedCall[]}
|
* @returns {TileOptions[]}
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // returns
|
* // returns
|
||||||
@@ -64,7 +82,7 @@ export class ParsedArg {
|
|||||||
*/
|
*/
|
||||||
export default function parse(input) {
|
export default function parse(input) {
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const pattern = /(\w+)\s*\(([^)]*)\)/g; // TODO: expand so identifiers can be more than just \w characters - also limit identifiers to a single letter (maybne)
|
const pattern = /(\w+)\s*\(([^)]*)\)/gu;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = pattern.exec(input)) !== null) {
|
while ((match = pattern.exec(input)) !== null) {
|
||||||
@@ -72,11 +90,10 @@ export default function parse(input) {
|
|||||||
const argsStr = match[2].trim();
|
const argsStr = match[2].trim();
|
||||||
const args = parseArguments(argsStr);
|
const args = parseArguments(argsStr);
|
||||||
|
|
||||||
// Hack to allow special characters in function names
|
// Hack to allow special characters in option names
|
||||||
// If function name is "__", then
|
// If the option name is "__", then the actual
|
||||||
// the actual function name is given by arg 0.
|
// option name is given by arg 0, and arg 0 is then
|
||||||
// Arg zero is automatically removed when the
|
// automatically removed.
|
||||||
// name is changed.
|
|
||||||
//
|
//
|
||||||
// So
|
// So
|
||||||
// __(foo, 1,2,3) === foo(1,2,3)
|
// __(foo, 1,2,3) === foo(1,2,3)
|
||||||
@@ -88,7 +105,7 @@ export default function parse(input) {
|
|||||||
name = args.shift().value;
|
name = args.shift().value;
|
||||||
}
|
}
|
||||||
|
|
||||||
calls.push(new ParsedCall(name, args));
|
calls.push(new TileOptions(name, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
return calls;
|
return calls;
|
||||||
@@ -96,12 +113,12 @@ export default function parse(input) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} argsStr
|
* @param {string} argsStr
|
||||||
* @returns {ParsedArg[]}
|
* @returns {TileArgs[]}
|
||||||
*/
|
*/
|
||||||
function parseArguments(argsStr) {
|
function parseArguments(argsStr) {
|
||||||
if (!argsStr) return [];
|
if (!argsStr) return [];
|
||||||
|
|
||||||
/** @type {ParsedArg[]} */
|
/** @type {TileArgs[]} */
|
||||||
const args = [];
|
const args = [];
|
||||||
const tokens = tokenize(argsStr);
|
const tokens = tokenize(argsStr);
|
||||||
|
|
||||||
@@ -109,9 +126,9 @@ function parseArguments(argsStr) {
|
|||||||
const token = tokens[pos];
|
const token = tokens[pos];
|
||||||
const namedMatch = token.match(/^(\w+)=(.+)$/);
|
const namedMatch = token.match(/^(\w+)=(.+)$/);
|
||||||
if (namedMatch) {
|
if (namedMatch) {
|
||||||
args.push(new ParsedArg(namedMatch[1], parseValue(namedMatch[2])));
|
args.push(new TileArgs(namedMatch[1], parseValue(namedMatch[2])));
|
||||||
} else {
|
} else {
|
||||||
args.push(new ParsedArg(Number.parseInt(pos), parseValue(token)));
|
args.push(new TileArgs(Number.parseInt(pos), parseValue(token)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +173,15 @@ function parseValue(str) {
|
|||||||
|
|
||||||
// Try to parse as number
|
// Try to parse as number
|
||||||
if (/^-?\d+(\.\d+)?$/.test(str)) {
|
if (/^-?\d+(\.\d+)?$/.test(str)) {
|
||||||
return parseFloat(str);
|
const f = parseFloat(str);
|
||||||
|
const rounded = Math.round(f);
|
||||||
|
const diff = Math.abs(rounded - f);
|
||||||
|
const epsilon = 1e-6; // MAGIC NUMBER
|
||||||
|
if (diff < epsilon) {
|
||||||
|
return rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Boolean
|
// Boolean
|
||||||
Reference in New Issue
Block a user