walaaaa
This commit is contained in:
@@ -81,281 +81,6 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* A class that creates an ASCII effect.
|
||||
*
|
||||
* The ASCII generation is based on [jsascii]{@link https://github.com/hassadee/jsascii/blob/master/jsascii.js}.
|
||||
*
|
||||
* @three_import import { AsciiEffect } from 'three/addons/effects/AsciiEffect.js';
|
||||
*/
|
||||
class AsciiEffect {
|
||||
/**
|
||||
* Constructs a new ASCII effect.
|
||||
*
|
||||
* @param {WebGLRenderer} renderer - The renderer.
|
||||
* @param {string} [charSet=' .:-=+*#%@'] - The char set.
|
||||
* @param {AsciiEffect~Options} [options] - The configuration parameter.
|
||||
*/
|
||||
constructor(renderer, charSet = " .:-=+*#%@", options = {}) {
|
||||
// ' .,:;=|iI+hHOE#`$';
|
||||
// darker bolder character set from https://github.com/saw/Canvas-ASCII-Art/
|
||||
// ' .\'`^",:;Il!i~+_-?][}{1)(|/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$'.split('');
|
||||
|
||||
// Some ASCII settings
|
||||
|
||||
const fResolution = options["resolution"] || 0.15;
|
||||
const iScale = options["scale"] || 1;
|
||||
const bColor = options["color"] || false;
|
||||
const bAlpha = options["alpha"] || false;
|
||||
const bBlock = options["block"] || false;
|
||||
const bInvert = options["invert"] || false;
|
||||
const strResolution = options["strResolution"] || "low";
|
||||
|
||||
let width, height;
|
||||
|
||||
const domElement = document.createElement("div");
|
||||
domElement.style.cursor = "default";
|
||||
|
||||
const oAscii = document.createElement("table");
|
||||
domElement.appendChild(oAscii);
|
||||
|
||||
let iWidth, iHeight;
|
||||
let oImg;
|
||||
|
||||
/**
|
||||
* Resizes the effect.
|
||||
*
|
||||
* @param {number} w - The width of the effect in logical pixels.
|
||||
* @param {number} h - The height of the effect in logical pixels.
|
||||
*/
|
||||
this.setSize = function (w, h) {
|
||||
width = w;
|
||||
height = h;
|
||||
|
||||
renderer.setSize(w, h);
|
||||
|
||||
initAsciiSize();
|
||||
};
|
||||
|
||||
/**
|
||||
* When using this effect, this method should be called instead of the
|
||||
* default {@link WebGLRenderer#render}.
|
||||
*
|
||||
* @param {Object3D} scene - The scene to render.
|
||||
* @param {Camera} camera - The camera.
|
||||
*/
|
||||
this.render = function (scene, camera) {
|
||||
renderer.render(scene, camera);
|
||||
asciifyImage(oAscii);
|
||||
};
|
||||
|
||||
/**
|
||||
* The DOM element of the effect. This element must be used instead of the
|
||||
* default {@link WebGLRenderer#domElement}.
|
||||
*
|
||||
* @type {HTMLDivElement}
|
||||
*/
|
||||
this.domElement = domElement;
|
||||
|
||||
// Throw in ascii library from https://github.com/hassadee/jsascii/blob/master/jsascii.js (MIT License)
|
||||
|
||||
function initAsciiSize() {
|
||||
iWidth = Math.floor(width * fResolution);
|
||||
iHeight = Math.floor(height * fResolution);
|
||||
|
||||
oCanvas.width = iWidth;
|
||||
oCanvas.height = iHeight;
|
||||
// oCanvas.style.display = "none";
|
||||
// oCanvas.style.width = iWidth;
|
||||
// oCanvas.style.height = iHeight;
|
||||
|
||||
oImg = renderer.domElement;
|
||||
|
||||
if (oImg.style.backgroundColor) {
|
||||
oAscii.rows[0].cells[0].style.backgroundColor = oImg.style.backgroundColor;
|
||||
oAscii.rows[0].cells[0].style.color = oImg.style.color;
|
||||
}
|
||||
|
||||
oAscii.cellSpacing = "0";
|
||||
oAscii.cellPadding = "0";
|
||||
|
||||
const oStyle = oAscii.style;
|
||||
oStyle.whiteSpace = "pre";
|
||||
oStyle.margin = "0px";
|
||||
oStyle.padding = "0px";
|
||||
oStyle.letterSpacing = fLetterSpacing + "px";
|
||||
oStyle.fontFamily = strFont;
|
||||
oStyle.fontSize = fFontSize + "px";
|
||||
oStyle.lineHeight = fLineHeight + "px";
|
||||
oStyle.textAlign = "left";
|
||||
oStyle.textDecoration = "none";
|
||||
}
|
||||
|
||||
const strFont = "courier new, monospace";
|
||||
|
||||
const oCanvasImg = renderer.domElement;
|
||||
|
||||
const oCanvas = document.createElement("canvas");
|
||||
if (!oCanvas.getContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oCtx = oCanvas.getContext("2d");
|
||||
if (!oCtx.getImageData) {
|
||||
return;
|
||||
}
|
||||
|
||||
let aCharList;
|
||||
if (charSet) {
|
||||
aCharList = charSet.split("");
|
||||
} else {
|
||||
const aDefaultCharList = " .,:;i1tfLCG08@".split("");
|
||||
const aDefaultColorCharList = " CGO08@".split("");
|
||||
aCharList = bColor ? aDefaultColorCharList : aDefaultCharList;
|
||||
}
|
||||
|
||||
// Setup dom
|
||||
|
||||
const fFontSize = (2 / fResolution) * iScale;
|
||||
const fLineHeight = (2 / fResolution) * iScale;
|
||||
|
||||
// adjust letter-spacing for all combinations of scale and resolution to get it to fit the image width.
|
||||
|
||||
let fLetterSpacing = 0;
|
||||
|
||||
if (strResolution == "low") {
|
||||
switch (iScale) {
|
||||
case 1:
|
||||
fLetterSpacing = -1;
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
fLetterSpacing = -2.1;
|
||||
break;
|
||||
case 4:
|
||||
fLetterSpacing = -3.1;
|
||||
break;
|
||||
case 5:
|
||||
fLetterSpacing = -4.15;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (strResolution == "medium") {
|
||||
switch (iScale) {
|
||||
case 1:
|
||||
fLetterSpacing = 0;
|
||||
break;
|
||||
case 2:
|
||||
fLetterSpacing = -1;
|
||||
break;
|
||||
case 3:
|
||||
fLetterSpacing = -1.04;
|
||||
break;
|
||||
case 4:
|
||||
case 5:
|
||||
fLetterSpacing = -2.1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (strResolution == "high") {
|
||||
switch (iScale) {
|
||||
case 1:
|
||||
case 2:
|
||||
fLetterSpacing = 0;
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
fLetterSpacing = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// can't get a span or div to flow like an img element, but a table works?
|
||||
|
||||
// convert img element to ascii
|
||||
|
||||
function asciifyImage(oAscii) {
|
||||
oCtx.clearRect(0, 0, iWidth, iHeight);
|
||||
oCtx.drawImage(oCanvasImg, 0, 0, iWidth, iHeight);
|
||||
const oImgData = oCtx.getImageData(0, 0, iWidth, iHeight).data;
|
||||
|
||||
// Coloring loop starts now
|
||||
let strChars = "";
|
||||
|
||||
// console.time('rendering');
|
||||
|
||||
for (let y = 0; y < iHeight; y += 2) {
|
||||
for (let x = 0; x < iWidth; x++) {
|
||||
const iOffset = (y * iWidth + x) * 4;
|
||||
|
||||
const iRed = oImgData[iOffset];
|
||||
const iGreen = oImgData[iOffset + 1];
|
||||
const iBlue = oImgData[iOffset + 2];
|
||||
const iAlpha = oImgData[iOffset + 3];
|
||||
let iCharIdx;
|
||||
|
||||
let fBrightness;
|
||||
|
||||
fBrightness = (0.3 * iRed + 0.59 * iGreen + 0.11 * iBlue) / 255;
|
||||
// fBrightness = (0.3*iRed + 0.5*iGreen + 0.3*iBlue) / 255;
|
||||
|
||||
if (iAlpha == 0) {
|
||||
// should calculate alpha instead, but quick hack :)
|
||||
//fBrightness *= (iAlpha / 255);
|
||||
fBrightness = 1;
|
||||
}
|
||||
|
||||
iCharIdx = Math.floor((1 - fBrightness) * (aCharList.length - 1));
|
||||
|
||||
if (bInvert) {
|
||||
iCharIdx = aCharList.length - iCharIdx - 1;
|
||||
}
|
||||
|
||||
// good for debugging
|
||||
//fBrightness = Math.floor(fBrightness * 10);
|
||||
//strThisChar = fBrightness;
|
||||
|
||||
let strThisChar = aCharList[iCharIdx];
|
||||
|
||||
if (strThisChar === undefined || strThisChar == " ") strThisChar = " ";
|
||||
|
||||
if (bColor) {
|
||||
strChars +=
|
||||
"<span style='" +
|
||||
"color:rgb(" +
|
||||
iRed +
|
||||
"," +
|
||||
iGreen +
|
||||
"," +
|
||||
iBlue +
|
||||
");" +
|
||||
(bBlock
|
||||
? "background-color:rgb(" + iRed + "," + iGreen + "," + iBlue + ");"
|
||||
: "") +
|
||||
(bAlpha ? "opacity:" + iAlpha / 255 + ";" : "") +
|
||||
"'>" +
|
||||
strThisChar +
|
||||
"</span>";
|
||||
} else {
|
||||
strChars += strThisChar;
|
||||
}
|
||||
}
|
||||
|
||||
strChars += "<br/>";
|
||||
}
|
||||
|
||||
oAscii.innerHTML = `<tr><td style="display:block;width:${width}px;height:${height}px;overflow:hidden">${strChars}</td></tr>`;
|
||||
|
||||
// console.timeEnd('rendering');
|
||||
|
||||
// return oAscii;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This type represents configuration settings of `AsciiEffect`.
|
||||
*
|
||||
|
||||
@@ -72,12 +72,12 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="gameContainer">
|
||||
<div id="viewport"></div>
|
||||
|
||||
<div id="threejs" style="border: 2px solid green; background-color: #222"></div>
|
||||
<div id="minimap"></div>
|
||||
|
||||
<div id="compass">orientation</div>
|
||||
|
||||
<div id="viewport"></div>
|
||||
|
||||
<div id="mapInput">
|
||||
←→↑↓
|
||||
<br />
|
||||
@@ -86,13 +86,13 @@
|
||||
############################################################
|
||||
############################################################
|
||||
## ################# ########################
|
||||
## # # ################# # ## ########################
|
||||
## #S ################# # ## ################
|
||||
## # # ################# # ## #### ####
|
||||
## # ### ################# # ## ########################
|
||||
## #S# ################# # ## ################
|
||||
## # # # ################# # ## #### ####
|
||||
## M # # ## # #### # # ####
|
||||
###### #################### ## #### # ####
|
||||
###### #################### # ## # # #### ####
|
||||
###### #################### # ####
|
||||
######M#################### # ####
|
||||
###### #################### ########## #### ####
|
||||
###### #################### ########## # # #### # # ####
|
||||
###### #################### ########## #### # # ####
|
||||
@@ -100,7 +100,7 @@
|
||||
###### #################### ############################
|
||||
###### #################### # ############################
|
||||
###### #################### # ############################
|
||||
###### #################### # ############################
|
||||
######M#################### # ############################
|
||||
###### ## ########### ############################
|
||||
###### ## ########### # # ############################
|
||||
###### ## ########### ######## ############
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Vector2i, Orientation, RelativeMovement, PI_OVER_TWO } from "./ascii_types.js";
|
||||
import { DefaultRendererOptions, FirstPersonRenderer } from "./ascii_first_person_renderer.js";
|
||||
import { MiniMapRenderer } from "../ascii_minimap_renderer.js";
|
||||
import { Texture } from "./ascii_textureloader.js";
|
||||
import { AsciiWindow } from "./ascii_window.js";
|
||||
import { TileMap } from "./ascii_tile_map.js";
|
||||
import eobWallUrl1 from "./eob1.png";
|
||||
@@ -141,8 +140,8 @@ class DungeonCrawler {
|
||||
return;
|
||||
}
|
||||
this.rendering.firstPersonRenderer.renderFrame(
|
||||
camX + 0.5, // add .5 to get camera into center of cell
|
||||
camY + 0.5, // 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
|
||||
angle,
|
||||
);
|
||||
}
|
||||
@@ -156,7 +155,7 @@ class DungeonCrawler {
|
||||
|
||||
this.map = TileMap.fromText(mapString);
|
||||
|
||||
this.player._posV = this.map.findFirst({ startLocation: true });
|
||||
this.player._posV = this.map.findFirst({ isStartLocation: true });
|
||||
|
||||
if (!this.player._posV) {
|
||||
throw new Error("Could not find a start location for the player");
|
||||
@@ -164,31 +163,18 @@ class DungeonCrawler {
|
||||
|
||||
this.rendering.miniMapRenderer = new MiniMapRenderer(this.rendering.minimapWindow, this.map);
|
||||
|
||||
const textureUrls = [eobWallUrl1, gnollSpriteUrl];
|
||||
const textures = new Array(textureUrls.length).fill();
|
||||
let textureLoadCount = 0;
|
||||
|
||||
textureUrls.forEach((url, textureId) => {
|
||||
Texture.fromSource(url).then((texture) => {
|
||||
textures[textureId] = texture;
|
||||
textureLoadCount++;
|
||||
|
||||
if (textureLoadCount < textureUrls.length) {
|
||||
return;
|
||||
}
|
||||
const textureFilenames = [eobWallUrl1, gnollSpriteUrl];
|
||||
this.rendering.firstPersonRenderer = new FirstPersonRenderer(
|
||||
this.rendering.firstPersonWindow,
|
||||
this.map,
|
||||
textures,
|
||||
textureFilenames,
|
||||
this.rendering.options,
|
||||
);
|
||||
this.rendering.firstPersonRenderer.onReady = () => {
|
||||
this.render();
|
||||
this.renderMinimap();
|
||||
this.renderCompass();
|
||||
|
||||
console.debug("renderer ready", textures);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
startTurnAnimation(quarterTurns = 1) {
|
||||
@@ -237,8 +223,8 @@ class DungeonCrawler {
|
||||
this.player._posV,
|
||||
this.player.angle,
|
||||
);
|
||||
this.delay += 250; // MAGIC NUMBER: Pause for a tenth of a second after hitting a wall
|
||||
return false;
|
||||
// this.delay += 250; // MAGIC NUMBER: Pause for a tenth of a second after hitting a wall
|
||||
// return false;
|
||||
}
|
||||
|
||||
this.animation = {
|
||||
@@ -267,10 +253,10 @@ class DungeonCrawler {
|
||||
KeyW: () => this.startMoveAnimation(RelativeMovement.FORWARD),
|
||||
ArrowUp: () => this.startMoveAnimation(RelativeMovement.FORWARD),
|
||||
ArrowDown: () => this.startMoveAnimation(RelativeMovement.BACKWARD),
|
||||
ArrowLeft: () => this.startTurnAnimation(-1),
|
||||
ArrowRight: () => this.startTurnAnimation(1),
|
||||
KeyQ: () => this.startTurnAnimation(-1),
|
||||
KeyE: () => this.startTurnAnimation(1),
|
||||
ArrowLeft: () => this.startTurnAnimation(1),
|
||||
ArrowRight: () => this.startTurnAnimation(-1),
|
||||
KeyQ: () => this.startTurnAnimation(1),
|
||||
KeyE: () => this.startTurnAnimation(-1),
|
||||
};
|
||||
this.keys.names = Object.keys(this.keys.handlers);
|
||||
|
||||
|
||||
@@ -1,429 +1,214 @@
|
||||
import { NRGBA } from "./ascii_textureloader.js";
|
||||
import { TileMap, Tile } from "./ascii_tile_map.js";
|
||||
import { AsciiWindow } from "./ascii_window.js";
|
||||
import * as THREE from "three";
|
||||
import eobWallUrl1 from "./eob1.png";
|
||||
import gnollSpriteUrl from "./gnoll.png";
|
||||
|
||||
/**
|
||||
* Which side of a tile did the ray strike
|
||||
*/
|
||||
export const Side = {
|
||||
X_AXIS: 0,
|
||||
Y_AXIS: 1,
|
||||
};
|
||||
class RayCollision {
|
||||
mapX = 0;
|
||||
mapY = 0;
|
||||
rayLength = 0;
|
||||
side = Side.X_AXIS;
|
||||
/** @type {Tile} */
|
||||
tile;
|
||||
}
|
||||
|
||||
class RayCastResult {
|
||||
hitWall = false;
|
||||
hitSprite = false;
|
||||
wallCollision = new RayCollision();
|
||||
|
||||
/** @type {RayCollision[]} */
|
||||
collisions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} FirstPersonRendererOptions
|
||||
* @property {string} wallChar
|
||||
* @property {NRGBA} floorColor
|
||||
* @property {string} floorChar
|
||||
* @property {NRGBA} ceilingColor
|
||||
* @property {string} ceilingChar
|
||||
* @property {number} viewDistance
|
||||
* @property {number} fov
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {FirstPersonRendererOptions}
|
||||
*/
|
||||
export const DefaultRendererOptions = {
|
||||
wallChar: "W",
|
||||
floorColor: new NRGBA(0.365, 0.165, 0.065),
|
||||
floorChar: "f",
|
||||
ceilingColor: new NRGBA(0.3, 0.3, 0.3),
|
||||
ceilingChar: "c",
|
||||
fadeOutColor: new NRGBA(0.3, 0.3, 0.3),
|
||||
viewDistance: 5,
|
||||
fov: Math.PI / 3, // 60 degrees - good for spooky
|
||||
|
||||
wallChar: "#",
|
||||
|
||||
floorColor: 0x654321,
|
||||
floorChar: "f",
|
||||
ceilingColor: 0x555555,
|
||||
ceilingChar: "c",
|
||||
fadeOutColor: 0x555555,
|
||||
};
|
||||
|
||||
export class FirstPersonRenderer {
|
||||
/**
|
||||
* @param {AsciiWindow} aWindow the window we render onto.
|
||||
* @param {TileMap} map
|
||||
* @param {Texture[]} textures
|
||||
* @param {FirstPersonRendererOptions} options
|
||||
* @param {string[]} textureFilenames
|
||||
*/
|
||||
constructor(aWindow, map, textures, options) {
|
||||
/** @constant @readonly @type {TileMap} */
|
||||
this.map = map;
|
||||
constructor(aWindow, map, textureFilenames, options) {
|
||||
const w = 600;
|
||||
const h = 400;
|
||||
|
||||
/** @constant @readonly @type {AsciiWindow} */
|
||||
this.window = aWindow;
|
||||
|
||||
/** @constant @readonly @type {number} */
|
||||
this.fov = options.fov ?? DefaultRendererOptions.fov;
|
||||
|
||||
/** @constant @readonly @type {number} */
|
||||
this.viewDistance = options.viewDistance ?? DefaultRendererOptions.viewDistance;
|
||||
|
||||
/** @constant @readonly @type {Texture[]} */
|
||||
this.textures = textures;
|
||||
this.window = aWindow;
|
||||
this.map = map;
|
||||
|
||||
/** @constant @readonly @type {string} */
|
||||
this.wallChar = options.wallChar ?? DefaultRendererOptions.wallChar;
|
||||
/** @constant @readonly @type {NRGBA} */
|
||||
this.floorColor = options.floorColor ?? DefaultRendererOptions.floorColor;
|
||||
/** @constant @readonly @type {string} */
|
||||
this.floorChar = options.floorChar ?? DefaultRendererOptions.floorChar;
|
||||
/** @constant @readonly @type {NRGBA} */
|
||||
this.ceilingColor = options.ceilingColor ?? DefaultRendererOptions.ceilingColor;
|
||||
/** @constant @readonly @type {string} */
|
||||
this.ceilingChar = options.ceilingChar ?? DefaultRendererOptions.ceilingChar;
|
||||
this.scene = new THREE.Scene();
|
||||
this.camera = new THREE.PerspectiveCamera((this.fov * 180) / Math.PI, w / h);
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: false }); // Do not anti-alias, it could interfere with the conversion to ascii
|
||||
|
||||
/**
|
||||
* Pre-computed colors to use when drawing floors, ceilings and "fadeout"
|
||||
*
|
||||
* There is one entry for every screen row.
|
||||
* Each entry contains a color to use when drawing floors, ceilings, and "fadeout".
|
||||
*
|
||||
* @constant @readonly @type {Array<Array<string>>}
|
||||
*/
|
||||
this.shades = [];
|
||||
//
|
||||
// Fog, Fadeout & Background
|
||||
//
|
||||
this.scene.background = new THREE.Color(0);
|
||||
this.scene.fog = new THREE.Fog(0, 0, this.viewDistance - 1);
|
||||
|
||||
/**
|
||||
* Pre-compute the shades variable
|
||||
*/
|
||||
this.computeShades();
|
||||
//
|
||||
// Camera
|
||||
//
|
||||
this.camera.up.set(0, 0, 1); // Z-up instead of Y-up
|
||||
|
||||
//
|
||||
// Torch
|
||||
//
|
||||
this.torch = new THREE.PointLight(0xffffff, 0.9, this.viewDistance, 2); // https://threejs.org/docs/#api/en/lights/PointLight
|
||||
this.torch.position.copy(this.camera.position);
|
||||
this.scene.add(this.torch);
|
||||
|
||||
//
|
||||
// Sprites
|
||||
//
|
||||
/** @type {THREE.Sprite[]} */
|
||||
this.sprites = [];
|
||||
|
||||
//
|
||||
this.initMap();
|
||||
|
||||
//
|
||||
this.renderer.setSize(w, h);
|
||||
document.getElementById("threejs").appendChild(this.renderer.domElement);
|
||||
this.renderFrame();
|
||||
}
|
||||
|
||||
computeShades() {
|
||||
const screenHeight = this.window.height;
|
||||
const halfScreenHeight = screenHeight / 2;
|
||||
const lineHeight = Math.floor(screenHeight / this.viewDistance);
|
||||
const minY = Math.floor(-lineHeight / 2 + halfScreenHeight); // if y lower than minY, then we're painting ceiling
|
||||
const maxY = Math.floor(lineHeight / 2 + halfScreenHeight); // if y higher than maxY then we're painting floor
|
||||
initMap() {
|
||||
const wallPlanes = [];
|
||||
const sprites = [];
|
||||
|
||||
for (let y = 0; y < screenHeight; y++) {
|
||||
if (y < minY) {
|
||||
/** @type {Map<number,Array} */
|
||||
this.map.forEach((/** @type {Tile} */ tile, /** @type {number} */ x, /** @type {number} y */ y) => {
|
||||
//
|
||||
// y is smaller than minY. This means we're painting above
|
||||
// the walls, i.e. painting the ceiling.
|
||||
// The closer y is to minY, the farther away this part of the
|
||||
// ceiling is.
|
||||
//
|
||||
// High diff => near
|
||||
// Low diff => far
|
||||
//
|
||||
const diff = minY - y;
|
||||
this.shades.push([this.ceilingChar, this.ceilingColor.mulledRGB(diff / minY).toCSS()]);
|
||||
} else if (y >= maxY) {
|
||||
//
|
||||
// Floor
|
||||
//
|
||||
const diff = y - maxY;
|
||||
this.shades.push([this.floorChar, this.floorColor.mulledRGB(diff / minY).toCSS()]);
|
||||
} else {
|
||||
//
|
||||
// The darkness at the end of the tunnel
|
||||
//
|
||||
this.shades.push([" ", "#000"]);
|
||||
if (tile.isStartLocation) {
|
||||
this.camera.position.set(x, y, 0);
|
||||
this.camera.lookAt(x, y - 1, 0);
|
||||
this.torch.position.copy(this.camera.position);
|
||||
|
||||
console.log("Initial Camera Position:", this.camera.position);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tile.isWall) {
|
||||
if (!this.map.isWall(x, y + 1)) {
|
||||
wallPlanes.push([x, y + 0.5, Math.PI * 0.0]);
|
||||
}
|
||||
if (!this.map.isWall(x + 1, y)) {
|
||||
wallPlanes.push([x + 0.5, y, Math.PI * 0.5]);
|
||||
}
|
||||
if (!this.map.isWall(x, y - 1)) {
|
||||
wallPlanes.push([x, y - 0.5, Math.PI * 1.0]);
|
||||
}
|
||||
if (!this.map.isWall(x - 1, y)) {
|
||||
wallPlanes.push([x - 0.5, y, Math.PI * 1.5]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tile.isSprite) {
|
||||
console.log("Sprite", tile);
|
||||
sprites.push([x, y, tile.textureId]);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Sprites, doors, etc
|
||||
});
|
||||
|
||||
//
|
||||
// Floor (XY plane at Z = -.5)
|
||||
//
|
||||
const floorGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
|
||||
const floorMat = new THREE.MeshStandardMaterial({ color: 0x964b00 /* side: THREE.DoubleSide */ });
|
||||
const floor = new THREE.Mesh(floorGeo, floorMat);
|
||||
floor.position.set(this.map.width / 2, this.map.height / 2, -0.5);
|
||||
this.scene.add(floor);
|
||||
|
||||
//
|
||||
// Ceiling (XY plane at Z = .5)
|
||||
//
|
||||
const ceilingGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
|
||||
const ceilingMat = new THREE.MeshStandardMaterial({ color: 0x333333, side: THREE.BackSide });
|
||||
const ceiling = new THREE.Mesh(ceilingGeo, ceilingMat);
|
||||
ceiling.position.set(this.map.width / 2, this.map.height / 2, 0.5);
|
||||
this.scene.add(ceiling);
|
||||
|
||||
//
|
||||
// Walls
|
||||
//
|
||||
const wallTex = new THREE.TextureLoader().load(eobWallUrl1, (texture) => {
|
||||
texture.magFilter = THREE.NearestFilter; // no smoothing when scaling up
|
||||
texture.minFilter = THREE.NearestFilter; // no mipmaps / no smoothing when scaling down
|
||||
texture.generateMipmaps = false; // don’t build mipmaps
|
||||
});
|
||||
|
||||
const wallGeo = new THREE.PlaneGeometry();
|
||||
wallGeo.rotateX(Math.PI / 2); // Get the geometry-plane the right way up (z-up)
|
||||
// wallGeo.rotateY(Math.PI); // rotate textures to be the right way up
|
||||
|
||||
const instancedMesh = new THREE.InstancedMesh(
|
||||
wallGeo,
|
||||
new THREE.MeshStandardMaterial({ map: wallTex }),
|
||||
wallPlanes.length,
|
||||
);
|
||||
this.scene.add(instancedMesh);
|
||||
|
||||
// Temp objects for generating matrices
|
||||
const dummy = new THREE.Object3D();
|
||||
|
||||
wallPlanes.forEach((coords, idx) => {
|
||||
const [x, y, rot] = coords;
|
||||
dummy.position.set(x, y, 0);
|
||||
dummy.rotation.set(Math.PI, 0, rot);
|
||||
dummy.updateMatrix();
|
||||
instancedMesh.setMatrixAt(idx, dummy.matrix);
|
||||
});
|
||||
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||
|
||||
//
|
||||
// Sprites
|
||||
//
|
||||
// Load a sprite texture
|
||||
|
||||
const tex = new THREE.TextureLoader().load(gnollSpriteUrl, (t) => {
|
||||
t.magFilter = THREE.NearestFilter; // pixel-art crisp
|
||||
t.minFilter = THREE.NearestFilter;
|
||||
t.generateMipmaps = false;
|
||||
t.wrapS = THREE.RepeatWrapping;
|
||||
t.wrapT = THREE.RepeatWrapping;
|
||||
t.repeat.set(1, 1);
|
||||
});
|
||||
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex, transparent: true });
|
||||
|
||||
for (const [x, y, textureId] of sprites) {
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.position.set(
|
||||
x,
|
||||
y,
|
||||
0, // z (stand on floor)
|
||||
);
|
||||
sprite.position.set(x, y, 0);
|
||||
this.sprites.push(sprite);
|
||||
this.scene.add(sprite);
|
||||
console.log({ x, y, textureId });
|
||||
}
|
||||
}
|
||||
|
||||
renderFrame(posX, posY, dirAngle, commit = true) {
|
||||
const benchmarkStart = performance.now();
|
||||
const screenWidth = this.window.width;
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
const lookAtV = new THREE.Vector3(1, 0, 0);
|
||||
lookAtV
|
||||
.applyAxisAngle(new THREE.Vector3(0, 0, 1), dirAngle)
|
||||
.normalize()
|
||||
.add(this.camera.position);
|
||||
|
||||
/** @type {Map<number,Tile} The coordinates of all the tiles checked while rendering this frame*/
|
||||
const coordsChecked = new Map();
|
||||
this.camera.position.x = posX;
|
||||
this.camera.position.y = posY;
|
||||
|
||||
for (let x = 0; x < screenWidth; x++) {
|
||||
const angleOffset = (x / screenWidth - 0.5) * this.fov; // in radians
|
||||
const rayAngle = dirAngle + angleOffset;
|
||||
const rayDirX = Math.cos(rayAngle);
|
||||
const rayDirY = Math.sin(rayAngle);
|
||||
|
||||
// Cast ray using our DDA function
|
||||
const ray = this.castRay(posX, posY, rayDirX, rayDirY, coordsChecked);
|
||||
this.torch.position.copy(this.camera.position);
|
||||
this.torch.position.z += 0.25;
|
||||
this.camera.lookAt(lookAtV);
|
||||
|
||||
//
|
||||
// Render a single screen column
|
||||
this.renderColumn(x, ray, rayDirX, rayDirY, angleOffset);
|
||||
}
|
||||
|
||||
const renderTime = performance.now() - benchmarkStart;
|
||||
|
||||
// Did it take more than 5ms to render the scene?
|
||||
if (renderTime > 5) {
|
||||
console.log("Rendering took a long time", { renderTime });
|
||||
}
|
||||
|
||||
if (commit) {
|
||||
requestAnimationFrame(() => {
|
||||
const benchmarkStart = performance.now();
|
||||
this.window.commitToDOM();
|
||||
const commitTime = performance.now() - benchmarkStart;
|
||||
if (commitTime > 5) {
|
||||
console.log("Updating DOM took a long time:", { commitTime });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a column on the screen where the ray hit a wall.
|
||||
* @param {number} x
|
||||
* @param {RayCastResult} ray
|
||||
* @param {number} rayDirX
|
||||
* @param {number} rayDirY
|
||||
* @param {number} angleOffset for far (in radians) is this column from the middle of the screen
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
renderColumn(x, ray, rayDirX, rayDirY, angleOffset) {
|
||||
// //
|
||||
// // Check if we hit anything at all
|
||||
// if (ray.collisions.length === 0) {
|
||||
// //
|
||||
// // We didn't hit anything. Just paint floor, wall, and darkness
|
||||
// for (let y = 0; y < this.window.height; y++) {
|
||||
// const [char, color] = this.shades[y];
|
||||
// this.window.put(x, y, char, color);
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // ALTERNATIVE always paint floor and ceiling
|
||||
for (let y = 0; y < this.window.height; y++) {
|
||||
const [char, color] = this.shades[y];
|
||||
this.window.put(x, y, char, color);
|
||||
}
|
||||
|
||||
for (const { rayLength, side, sampleU, tile } of ray.collisions) {
|
||||
let distance = Math.max(rayLength * Math.cos(angleOffset), 1e-12); // Avoid divide by zero
|
||||
|
||||
//
|
||||
// Calculate perspective.
|
||||
//
|
||||
const screenHeight = this.window.height;
|
||||
const lineHeight = Math.round(screenHeight / distance); // using round() because floor() gives aberrations when distance == (n + 0.500)
|
||||
const halfScreenHeight = screenHeight / 2;
|
||||
const halfLineHeight = lineHeight / 2;
|
||||
|
||||
let minY = Math.floor(halfScreenHeight - halfLineHeight);
|
||||
let maxY = Math.floor(halfScreenHeight + halfLineHeight);
|
||||
let unsafeMinY = minY; // can be lower than zero - it happens when we get so close to a wall we cannot see top or bottom
|
||||
|
||||
if (minY < 0) {
|
||||
minY = 0;
|
||||
}
|
||||
if (maxY >= screenHeight) {
|
||||
maxY = screenHeight - 1;
|
||||
}
|
||||
|
||||
//
|
||||
// Pick texture (here grid value decides which texture)
|
||||
//
|
||||
const texture = this.textures[tile.textureId];
|
||||
|
||||
for (let y = 0; y < screenHeight; y++) {
|
||||
//
|
||||
// Are we hitting the ceiling?
|
||||
//
|
||||
if (y < minY || y > maxY) {
|
||||
const [char, color] = this.shades[y];
|
||||
this.window.put(x, y, char, color);
|
||||
continue;
|
||||
}
|
||||
// // DEBUG LINES
|
||||
// if (y === minY) {
|
||||
// this.window.put(x, y, "m", "#0F0");
|
||||
// continue;
|
||||
// }
|
||||
// if (y === maxY) {
|
||||
// this.window.put(x, y, "M", "#F00");
|
||||
// continue;
|
||||
// }
|
||||
|
||||
//
|
||||
// Map screen y to texture y
|
||||
let sampleV = (y - unsafeMinY) / lineHeight; // y- coordinate of the texture point to sample
|
||||
|
||||
const color = texture.sample(sampleU, sampleV);
|
||||
if (!Number.isFinite(color.a)) {
|
||||
throw new Error("Waaat");
|
||||
}
|
||||
|
||||
if (color.a === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//
|
||||
// North-south walls are shaded differently from east-west walls
|
||||
let shade = side === Side.X_AXIS ? 0.8 : 1.0; // MAGIC NUMBERS
|
||||
|
||||
//
|
||||
// Dim walls that are far away
|
||||
const lightLevel = 1 - rayLength / this.viewDistance;
|
||||
|
||||
//
|
||||
// Darken the image
|
||||
color.mulRGB(shade * lightLevel);
|
||||
|
||||
this.window.put(x, y, tile.sprite ? "#" : this.wallChar, color.toCSS()); // MAGIC CONSTANT "S"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} camX x-coordinate of the camera (is the same
|
||||
* @param {number} camY y-coordinate of the camera
|
||||
* @param {number} dirX x-coordinate of the normalized vector of the viewing direction of the camera.
|
||||
* @param {number} dirX y-coordinate of the normalized vector of the viewing direction of the camera.
|
||||
* @param {Set<number>} coodsChecked
|
||||
*
|
||||
* @returns {RayCastResult}
|
||||
*
|
||||
*/
|
||||
castRay(camX, camY, dirX, dirY, coordsChecked) {
|
||||
// Current map square
|
||||
let mapX = Math.floor(camX);
|
||||
let mapY = Math.floor(camY);
|
||||
|
||||
// Length of ray from one x or y-side to next x or y-side
|
||||
const deltaDistX = dirX === 0 ? 1e15 : Math.abs(1 / dirX);
|
||||
const deltaDistY = dirY === 0 ? 1e15 : Math.abs(1 / dirY);
|
||||
|
||||
// Step direction (+1 or -1) and initial sideDist[XY]
|
||||
let stepX; // When DDA takes a horizontal step (on the map), how far should it move?
|
||||
let stepY; // When DDA takes a vertical step (on the map), how far should it move?
|
||||
let sideDistX; // How far has the ray moved horizontally (on the map) ?
|
||||
let sideDistY; // How far has the ray moved vertically (on the map) ?
|
||||
let side = Side.X_AXIS;
|
||||
|
||||
//
|
||||
// Calculate how to move along the x-axis
|
||||
if (dirX < 0) {
|
||||
stepX = -1; // step left along the x-axis
|
||||
sideDistX = (camX - mapX) * deltaDistX; // we've moved from the camera to the left edge of the tile
|
||||
} else {
|
||||
stepX = 1; // step right along the x-axis
|
||||
sideDistX = (mapX + 1.0 - camX) * deltaDistX; // we've moved from the camera to the right edge of the tile
|
||||
}
|
||||
|
||||
//
|
||||
// Calculate how to move along the y-axis
|
||||
if (dirY < 0) {
|
||||
stepY = -1; // // step down along the y-axis
|
||||
sideDistY = (camY - mapY) * deltaDistY; // we've move from the camera to the bottom edge of the tile
|
||||
} else {
|
||||
stepY = 1; // // step up along the y-axis
|
||||
sideDistY = (mapY + 1.0 - camY) * deltaDistY; // we've moved from the camera to the top edge of the tile
|
||||
}
|
||||
|
||||
/**
|
||||
* Any sprites the ray has hit on its way.
|
||||
* They are ordered in reverse order of closeness to the camera,
|
||||
* so that if they are drawn in their array ordering, they will
|
||||
* appear in the correct order on the screen.
|
||||
*
|
||||
* @type {RayCastResult}
|
||||
*/
|
||||
const result = new RayCastResult();
|
||||
|
||||
// DDA loop
|
||||
while (!result.hitWall) {
|
||||
//
|
||||
// Check if ray is longer than viewDistance
|
||||
if (Math.min(sideDistX, sideDistY) > this.viewDistance) {
|
||||
return result;
|
||||
}
|
||||
|
||||
//
|
||||
// Check for out of bounds
|
||||
if (mapX < 0 || mapX >= this.map.width || mapY < 0 || mapY >= this.map.height) {
|
||||
return result;
|
||||
}
|
||||
|
||||
let wallDist, sampleU;
|
||||
|
||||
//
|
||||
// Should we step in the x- or y-direction
|
||||
// DDA dictates we always move along the shortest vector
|
||||
if (sideDistX < sideDistY) {
|
||||
//
|
||||
// Move horizontally
|
||||
//
|
||||
sideDistX += deltaDistX;
|
||||
mapX += stepX;
|
||||
side = Side.X_AXIS;
|
||||
// Ray hit the east or west edge of the wall-tile
|
||||
wallDist = (mapX - camX + (1 - stepX) / 2) / dirX;
|
||||
sampleU = (camY + wallDist * dirY) % 1;
|
||||
|
||||
if (dirX > 0) {
|
||||
sampleU = 1 - sampleU;
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// Move vertically
|
||||
//
|
||||
sideDistY += deltaDistY;
|
||||
mapY += stepY;
|
||||
side = Side.Y_AXIS;
|
||||
// Ray hit the north or south edge of the wall-tile
|
||||
wallDist = (mapY - camY + (1 - stepY) / 2) / dirY;
|
||||
sampleU = (camX + wallDist * dirX) % 1;
|
||||
if (dirY < 0) {
|
||||
sampleU = 1 - sampleU;
|
||||
}
|
||||
}
|
||||
|
||||
const tile = this.map.get(mapX, mapY);
|
||||
coordsChecked.set(this.map.tileIdx(mapX, mapY), tile);
|
||||
|
||||
//
|
||||
// --------------------------
|
||||
// No collision? Move on
|
||||
// --------------------------
|
||||
if (!tile.collision) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rayLength = Math.hypot(
|
||||
wallDist * dirX, //
|
||||
wallDist * dirY, //
|
||||
);
|
||||
|
||||
//
|
||||
// Prepend the element to the array so rear-most sprites
|
||||
// appear first in the array,
|
||||
// enabling us to simply draw from back to front
|
||||
const collision = new RayCollision();
|
||||
collision.mapX = mapX;
|
||||
collision.mapY = mapY;
|
||||
collision.rayLength = rayLength;
|
||||
collision.tile = tile;
|
||||
collision.sampleU = sampleU;
|
||||
collision.side = side;
|
||||
result.collisions.unshift(collision);
|
||||
|
||||
//
|
||||
// --------------------------------
|
||||
// Algorithm stops if the ray hits
|
||||
// a wall.
|
||||
// -------------------------------
|
||||
if (tile.wall) {
|
||||
result.hitWall = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
export class NRGBA {
|
||||
//
|
||||
constructor(r = 0, g = 0, b = 0, a = 1) {
|
||||
this.r = r;
|
||||
this.g = g;
|
||||
this.b = b;
|
||||
this.a = a;
|
||||
}
|
||||
|
||||
mulRGB(factor) {
|
||||
this.r *= factor;
|
||||
this.g *= factor;
|
||||
this.b *= factor;
|
||||
}
|
||||
|
||||
mulledRGB(factor) {
|
||||
return new NRGBA(this.r * factor, this.g * factor, this.b * factor, this.a);
|
||||
}
|
||||
|
||||
get dR() {
|
||||
return ((this.r * 255) | 0) % 256;
|
||||
}
|
||||
get dG() {
|
||||
return ((this.g * 255) | 0) % 256;
|
||||
}
|
||||
get dB() {
|
||||
return ((this.b * 255) | 0) % 256;
|
||||
}
|
||||
get dA() {
|
||||
return ((this.a * 255) | 0) % 256;
|
||||
}
|
||||
|
||||
toCSS() {
|
||||
return (
|
||||
"#" + // prefix
|
||||
this.dR.toString(16).padStart(2, "0") +
|
||||
this.dG.toString(16).padStart(2, "0") +
|
||||
this.dB.toString(16).padStart(2, "0") +
|
||||
this.dA.toString(16).padStart(2, "0")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Texture class,
|
||||
* represents a single texture
|
||||
*/
|
||||
export class Texture {
|
||||
static async fromSource(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
|
||||
const result = new Texture(img.width, img.height, imageData.data);
|
||||
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => reject(new Error(`Failed to load texture: ${src}`));
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
constructor(width, height, data) {
|
||||
/** @type {number} */
|
||||
this.width = width;
|
||||
/** @type {number} */
|
||||
this.height = height;
|
||||
/** @type {Uint8ClampedArray} */
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bilinear sampling for smooth texture filtering
|
||||
*
|
||||
* @param {number} u the "x" coordinate of the texture sample pixel. Normalized to [0...1]
|
||||
* @param {number} w the "y" coordinate of the texture sample pixel. Normalized to [0...1]
|
||||
*
|
||||
* @returns {NRGBA}
|
||||
*/
|
||||
sample(u, v) {
|
||||
const x = Math.min(this.width - 1, Math.round(u * this.width));
|
||||
const y = Math.min(this.height - 1, Math.round(v * this.height));
|
||||
const index = (y * this.width + x) * 4;
|
||||
return new NRGBA(
|
||||
this.data[index + 0] / 255,
|
||||
this.data[index + 1] / 255,
|
||||
this.data[index + 2] / 255,
|
||||
this.data[index + 3] / 255,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Vector2i, Orientation } from "./ascii_types.js";
|
||||
import { AsciiWindow } from "./ascii_window.js";
|
||||
import { Texture } from "./ascii_textureloader.js";
|
||||
|
||||
export class Tile {
|
||||
/** @type {string} How should this tile be rendered on the minimap.*/
|
||||
@@ -10,16 +9,16 @@ export class Tile {
|
||||
minimapColor = "#fff";
|
||||
|
||||
/** @type {boolean} Should this be rendered as a wall? */
|
||||
wall = false;
|
||||
isWall = false;
|
||||
|
||||
/** @type {boolean} is this tile occupied by a sprite? */
|
||||
sprite = false;
|
||||
isSprite = false;
|
||||
|
||||
/** @type {boolean} Can the player walk here? */
|
||||
traversable = true;
|
||||
|
||||
/** @type {boolean} Is this where they player starts? */
|
||||
startLocation = false;
|
||||
isStartLocation = false;
|
||||
|
||||
/** @type {boolean} Is this where they player starts? */
|
||||
textureId = 0;
|
||||
@@ -34,7 +33,7 @@ export class Tile {
|
||||
}
|
||||
|
||||
get collision() {
|
||||
return this.wall || this.sprite;
|
||||
return this.isWall || this.isSprite;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +44,7 @@ export const defaultLegend = Object.freeze({
|
||||
"": new Tile({
|
||||
minimapChar: " ",
|
||||
traversable: true,
|
||||
wall: false,
|
||||
isWall: false,
|
||||
}),
|
||||
|
||||
//
|
||||
@@ -53,14 +52,14 @@ export const defaultLegend = Object.freeze({
|
||||
" ": new Tile({
|
||||
minimapChar: " ",
|
||||
traversable: true,
|
||||
wall: false,
|
||||
isWall: false,
|
||||
}),
|
||||
//
|
||||
// Default wall
|
||||
"#": new Tile({
|
||||
minimapChar: "#",
|
||||
traversable: false,
|
||||
wall: true,
|
||||
isWall: true,
|
||||
textureId: 0,
|
||||
}),
|
||||
|
||||
@@ -69,8 +68,8 @@ export const defaultLegend = Object.freeze({
|
||||
minimapChar: "M",
|
||||
minimapColor: "#f00",
|
||||
traversable: false,
|
||||
wall: false,
|
||||
sprite: true,
|
||||
isWall: false,
|
||||
isSprite: true,
|
||||
}),
|
||||
|
||||
//
|
||||
@@ -78,15 +77,15 @@ export const defaultLegend = Object.freeze({
|
||||
"Ω": new Tile({
|
||||
minimapChar: "#",
|
||||
traversable: true,
|
||||
wall: true,
|
||||
isWall: true,
|
||||
}),
|
||||
//
|
||||
// where the player starts
|
||||
"S": new Tile({
|
||||
minimapChar: "S", // "Š",
|
||||
traversable: true,
|
||||
wall: false,
|
||||
startLocation: true,
|
||||
isWall: false,
|
||||
isStartLocation: true,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -150,7 +149,7 @@ export class TileMap {
|
||||
/** @constant @readonly @type {Tile[][]} */
|
||||
this.tiles = tiles;
|
||||
/** @type {Tile} when probing a coordinate outside the map, this is the tile that is returned */
|
||||
this.outOfBoundsWall = this.findFirst({ wall: true });
|
||||
this.outOfBoundsWall = this.findFirst({ isWall: true });
|
||||
}
|
||||
|
||||
toString() {
|
||||
@@ -185,7 +184,7 @@ export class TileMap {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.tiles[y][x].wall;
|
||||
return this.tiles[y][x].isWall;
|
||||
}
|
||||
|
||||
findFirst(criteria) {
|
||||
@@ -242,8 +241,29 @@ export class TileMap {
|
||||
getAreaAround(x, y, radius) {
|
||||
return this.getArea(x - radius, y - radius, x + radius, y + radius);
|
||||
}
|
||||
|
||||
isVisible(x, y) {
|
||||
//
|
||||
// At least one of the four cardinal neighbours
|
||||
// must be non-wall in order for a tile to be
|
||||
// visible
|
||||
if (!this.isWall(x - 1, y)) {
|
||||
return true;
|
||||
}
|
||||
if (!this.isWall(x + 1, y)) {
|
||||
return true;
|
||||
}
|
||||
if (!this.isWall(x, y - 1)) {
|
||||
return true;
|
||||
}
|
||||
if (!this.isWall(x, y + 1)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.PI < 0 && AsciiWindow && Texture && Orientation) {
|
||||
if (Math.PI < 0 && AsciiWindow && Orientation) {
|
||||
("STFU Linda");
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ export const PI_OVER_TWO = Math.PI / 2;
|
||||
export const Orientation = {
|
||||
/** @constant @readonly @type {number} Going east increases X */
|
||||
EAST: 0,
|
||||
/** @constant @readonly @type {number} Going south increases Y */
|
||||
/** @constant @readonly @type {number} Going south decreases Y */
|
||||
SOUTH: 1,
|
||||
/** @constant @readonly @type {number} Going west decreases X */
|
||||
WEST: 2,
|
||||
/** @constant @readonly @type {number} Going south decreases Y */
|
||||
/** @constant @readonly @type {number} Going south increases Y */
|
||||
NORTH: 3,
|
||||
};
|
||||
|
||||
@@ -22,9 +22,9 @@ export const Orientation = {
|
||||
*/
|
||||
export const RelativeMovement = {
|
||||
FORWARD: 0,
|
||||
LEFT: 3,
|
||||
LEFT: 1,
|
||||
BACKWARD: 2,
|
||||
RIGHT: 1,
|
||||
RIGHT: 3,
|
||||
};
|
||||
|
||||
export class Vector2i {
|
||||
|
||||
6
node_modules/.package-lock.json
generated
vendored
6
node_modules/.package-lock.json
generated
vendored
@@ -3262,6 +3262,12 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.180.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
||||
26
node_modules/.vite/deps/_metadata.json
generated
vendored
26
node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -1,15 +1,31 @@
|
||||
{
|
||||
"hash": "a9cc42de",
|
||||
"hash": "5eac6a41",
|
||||
"configHash": "86a557ed",
|
||||
"lockfileHash": "56518f4e",
|
||||
"browserHash": "f6412460",
|
||||
"lockfileHash": "3ceab950",
|
||||
"browserHash": "20105502",
|
||||
"optimized": {
|
||||
"sprintf-js": {
|
||||
"src": "../../sprintf-js/src/sprintf.js",
|
||||
"file": "sprintf-js.js",
|
||||
"fileHash": "41b15421",
|
||||
"fileHash": "089e5f0c",
|
||||
"needsInterop": true
|
||||
},
|
||||
"three": {
|
||||
"src": "../../three/build/three.module.js",
|
||||
"file": "three.js",
|
||||
"fileHash": "22510eaa",
|
||||
"needsInterop": false
|
||||
},
|
||||
"three/src/math/MathUtils.js": {
|
||||
"src": "../../three/src/math/MathUtils.js",
|
||||
"file": "three_src_math_MathUtils__js.js",
|
||||
"fileHash": "f611651c",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {}
|
||||
"chunks": {
|
||||
"chunk-BUSYA2B4": {
|
||||
"file": "chunk-BUSYA2B4.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
node_modules/.vite/deps/sprintf-js.js
generated
vendored
7
node_modules/.vite/deps/sprintf-js.js
generated
vendored
@@ -1,7 +1,6 @@
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
import {
|
||||
__commonJS
|
||||
} from "./chunk-BUSYA2B4.js";
|
||||
|
||||
// node_modules/sprintf-js/src/sprintf.js
|
||||
var require_sprintf = __commonJS({
|
||||
|
||||
2
node_modules/.vite/deps/sprintf-js.js.map
generated
vendored
2
node_modules/.vite/deps/sprintf-js.js.map
generated
vendored
File diff suppressed because one or more lines are too long
7
package-lock.json
generated
7
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"figlet": "^1.8.2",
|
||||
"sprintf-js": "^1.1.3",
|
||||
"three": "^0.180.0",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3212,6 +3213,12 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/three": {
|
||||
"version": "0.180.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz",
|
||||
"integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"dependencies": {
|
||||
"figlet": "^1.8.2",
|
||||
"sprintf-js": "^1.1.3",
|
||||
"three": "^0.180.0",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -126,3 +126,7 @@ export class Scene {
|
||||
|
||||
onColon__hi = "Hoe";
|
||||
}
|
||||
|
||||
if (Math.PI < 0 && Session && Prompt) {
|
||||
("STFU Linda");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user