Wang
This commit is contained in:
@@ -72,12 +72,11 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="gameContainer">
|
<div id="gameContainer">
|
||||||
<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="viewport"></div>
|
||||||
|
<div id="compass">orientation</div>
|
||||||
|
<div id="minimap"></div>
|
||||||
|
|
||||||
|
<div id="threejs" style="border: 2px solid green; background-color: #222"></div>
|
||||||
<div id="mapInput">
|
<div id="mapInput">
|
||||||
←→↑↓
|
←→↑↓
|
||||||
<br />
|
<br />
|
||||||
@@ -87,12 +86,12 @@
|
|||||||
############################################################
|
############################################################
|
||||||
## ################# ########################
|
## ################# ########################
|
||||||
## # ### ################# # ## ########################
|
## # ### ################# # ## ########################
|
||||||
## #S# ################# # ## ################
|
## #P# Z###############Z # ## ################ ::: P(north) Z(1) Z(1) ;; Comments
|
||||||
## # # # ################# # ## #### ####
|
## # # # ################# # ## #### ####
|
||||||
## M # # ## # #### # # ####
|
## E # # ## # #### # # #### ::: E(Gnolls)
|
||||||
###### #################### ## #### # ####
|
###### #################### ## #### # ####
|
||||||
###### #################### # ## # # #### ####
|
###### #################### # ## # # #### ####
|
||||||
######M#################### # ####
|
######E#################### # #### ::: E(Goblins) These are comments
|
||||||
###### #################### ########## #### ####
|
###### #################### ########## #### ####
|
||||||
###### #################### ########## # # #### # # ####
|
###### #################### ########## # # #### # # ####
|
||||||
###### #################### ########## #### # # ####
|
###### #################### ########## #### # # ####
|
||||||
@@ -100,23 +99,23 @@
|
|||||||
###### #################### ############################
|
###### #################### ############################
|
||||||
###### #################### # ############################
|
###### #################### # ############################
|
||||||
###### #################### # ############################
|
###### #################### # ############################
|
||||||
######M#################### # ############################
|
######E#################### # ############################ ::: E(Minotaur)
|
||||||
###### ## ########### ############################
|
###### ## ##### ## ############################
|
||||||
###### ## ########### # # ############################
|
###### ## Z#### ## # # ############################ ::: Z(2) // Channel 2
|
||||||
###### ## ########### ######## ############
|
###### ## ####Z ## ######## ############ ::: Z(2) // Channel 2
|
||||||
###### ## ## # ########### ## ######## ############
|
###### ## ## # ########### ## ######## ############
|
||||||
###### ## # # ############
|
######E## # #E ############ ::: E(Dwarf) ; E(Gelatinous_Cube)
|
||||||
###### # # # ############
|
###### # # # ############
|
||||||
######### # ## ########### # ######### # ############
|
######### # ## ########### # ######### # ############
|
||||||
######### # # ########### # ######### # # ############
|
######### # # ########### # ######### # # ############
|
||||||
######### ########### # ######### ############
|
######### ########### # ######### ############
|
||||||
########################### # ######### #### ###############
|
###########O############### # ######### #### ############### ::: O(1)
|
||||||
########################### # ######### #### ###############
|
########################### # ######### #### ###############
|
||||||
########################### # ######### #### ###############
|
########################### # ######### #### ###############
|
||||||
########################### # ######### #### ###############
|
########################### # ######### #### ###############
|
||||||
########################### # #### ###############
|
########################### # #### ###############
|
||||||
######################### # #### # # # ######## ###
|
######################### # #### # # # ######## ###
|
||||||
######################### # # ######## # ###
|
########################o # # ######## # ### ::: o:2
|
||||||
######################### # ##### # # # # ######## ###
|
######################### # ##### # # # # ######## ###
|
||||||
######################### # # ###
|
######################### # # ###
|
||||||
######################### ####################### # ###
|
######################### ####################### # ###
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import { DefaultRendererOptions, FirstPersonRenderer } from "./ascii_first_perso
|
|||||||
import { MiniMapRenderer } from "../ascii_minimap_renderer.js";
|
import { MiniMapRenderer } from "../ascii_minimap_renderer.js";
|
||||||
import { AsciiWindow } from "./ascii_window.js";
|
import { AsciiWindow } from "./ascii_window.js";
|
||||||
import { TileMap } from "./ascii_tile_map.js";
|
import { TileMap } from "./ascii_tile_map.js";
|
||||||
import eobWallUrl1 from "./eob1.png";
|
|
||||||
import gnollSpriteUrl from "./gnoll.png";
|
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
|
|
||||||
class Player {
|
class Player {
|
||||||
@@ -109,7 +107,7 @@ class DungeonCrawler {
|
|||||||
/** @type {FirstPersonRenderer} */ firstPersonRenderer: null,
|
/** @type {FirstPersonRenderer} */ firstPersonRenderer: null,
|
||||||
/** @type {MiniMapRenderer} */ miniMapRenderer: null,
|
/** @type {MiniMapRenderer} */ miniMapRenderer: null,
|
||||||
|
|
||||||
firstPersonWindow: new AsciiWindow(document.getElementById("viewport"), 100, 45), // MAGIC CONSTANTS
|
firstPersonWindow: new AsciiWindow(document.getElementById("viewport"), 80, 45), // MAGIC CONSTANTS
|
||||||
minimapWindow: new AsciiWindow(document.getElementById("minimap"), 9, 9), // MAGIC CONSTANT
|
minimapWindow: new AsciiWindow(document.getElementById("minimap"), 9, 9), // MAGIC CONSTANT
|
||||||
|
|
||||||
options: DefaultRendererOptions,
|
options: DefaultRendererOptions,
|
||||||
@@ -163,11 +161,10 @@ class DungeonCrawler {
|
|||||||
|
|
||||||
this.rendering.miniMapRenderer = new MiniMapRenderer(this.rendering.minimapWindow, this.map);
|
this.rendering.miniMapRenderer = new MiniMapRenderer(this.rendering.minimapWindow, this.map);
|
||||||
|
|
||||||
const textureFilenames = [eobWallUrl1, gnollSpriteUrl];
|
|
||||||
this.rendering.firstPersonRenderer = new FirstPersonRenderer(
|
this.rendering.firstPersonRenderer = new FirstPersonRenderer(
|
||||||
this.rendering.firstPersonWindow,
|
this.rendering.firstPersonWindow,
|
||||||
this.map,
|
this.map,
|
||||||
textureFilenames,
|
["./eobBlueWall.png", "gnoll.png"], // textures
|
||||||
this.rendering.options,
|
this.rendering.options,
|
||||||
);
|
);
|
||||||
this.rendering.firstPersonRenderer.onReady = () => {
|
this.rendering.firstPersonRenderer.onReady = () => {
|
||||||
@@ -216,15 +213,15 @@ class DungeonCrawler {
|
|||||||
|
|
||||||
//
|
//
|
||||||
// We cant move into walls
|
// We cant move into walls
|
||||||
if (this.map.isWall(targetV.x, targetV.y)) {
|
if (!this.map.isTraversable(targetV.x, targetV.y)) {
|
||||||
console.info(
|
console.info(
|
||||||
"bumped into wall at %s (mypos: %s), direction=%d",
|
"bumped into an obstacle at %s (mypos: %s), direction=%d",
|
||||||
targetV,
|
targetV,
|
||||||
this.player._posV,
|
this.player._posV,
|
||||||
this.player.angle,
|
this.player.angle,
|
||||||
);
|
);
|
||||||
// this.delay += 250; // MAGIC NUMBER: Pause for a tenth of a second after hitting a wall
|
this.delay += 250; // MAGIC NUMBER: Pause for a bit after hitting an obstacle
|
||||||
// return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.animation = {
|
this.animation = {
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
import { TileMap, Tile } from "./ascii_tile_map.js";
|
import { TileMap } from "./ascii_tile_map.js";
|
||||||
|
import { Tile } from "./ascii_tile_types.js";
|
||||||
import { AsciiWindow } from "./ascii_window.js";
|
import { AsciiWindow } from "./ascii_window.js";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import eobWallUrl1 from "./eob1.png";
|
import { Vector3 } from "three";
|
||||||
import gnollSpriteUrl from "./gnoll.png";
|
|
||||||
|
|
||||||
export const DefaultRendererOptions = {
|
export const DefaultRendererOptions = {
|
||||||
viewDistance: 5,
|
viewDistance: 5,
|
||||||
fov: Math.PI / 3, // 60 degrees - good for spooky
|
fov: 60, // degrees
|
||||||
|
|
||||||
wallChar: "#",
|
|
||||||
|
|
||||||
floorColor: 0x654321,
|
floorColor: 0x654321,
|
||||||
floorChar: "f",
|
|
||||||
ceilingColor: 0x555555,
|
ceilingColor: 0x555555,
|
||||||
ceilingChar: "c",
|
|
||||||
fadeOutColor: 0x555555,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FirstPersonRenderer {
|
export class FirstPersonRenderer {
|
||||||
@@ -24,37 +19,64 @@ export class FirstPersonRenderer {
|
|||||||
* @param {string[]} textureFilenames
|
* @param {string[]} textureFilenames
|
||||||
*/
|
*/
|
||||||
constructor(aWindow, map, textureFilenames, options) {
|
constructor(aWindow, map, textureFilenames, options) {
|
||||||
const w = 600;
|
this.map = map;
|
||||||
const h = 400;
|
this.window = aWindow;
|
||||||
|
|
||||||
|
this.widthPx = aWindow.htmlElement.clientWidth;
|
||||||
|
this.heightPx = aWindow.htmlElement.clientHeight;
|
||||||
|
this.asciiWidth = aWindow.width;
|
||||||
|
this.asciiHeight = aWindow.height;
|
||||||
|
this.aaspect = this.widthPx / this.heightPx;
|
||||||
|
|
||||||
this.fov = options.fov ?? DefaultRendererOptions.fov;
|
this.fov = options.fov ?? DefaultRendererOptions.fov;
|
||||||
this.viewDistance = options.viewDistance ?? DefaultRendererOptions.viewDistance;
|
this.viewDistance = options.viewDistance ?? DefaultRendererOptions.viewDistance;
|
||||||
|
this.floorColor = options.floorColor ?? DefaultRendererOptions.floorColor;
|
||||||
this.window = aWindow;
|
this.ceilingColor = options.ceilingColor ?? DefaultRendererOptions.ceilingColor;
|
||||||
this.map = map;
|
|
||||||
|
|
||||||
this.scene = new THREE.Scene();
|
this.scene = new THREE.Scene();
|
||||||
this.camera = new THREE.PerspectiveCamera((this.fov * 180) / Math.PI, w / h);
|
this.mainCamera = new THREE.PerspectiveCamera(this.fov, this.aspect, 0.1, this.viewDistance);
|
||||||
this.renderer = new THREE.WebGLRenderer({ antialias: false }); // Do not anti-alias, it could interfere with the conversion to ascii
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
|
antialias: false,
|
||||||
|
preserveDrawingBuffer: true,
|
||||||
|
}); // Do not anti-alias, it could interfere with the conversion to ascii
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render buffer
|
||||||
|
//
|
||||||
|
this.bufferCanvas = document.createElement("canvas");
|
||||||
|
this.bufferCanvas.width = this.asciiWidth;
|
||||||
|
this.bufferCanvas.height = this.asciiHeight;
|
||||||
|
this.bufferContext = this.bufferCanvas.getContext("2d");
|
||||||
|
|
||||||
//
|
//
|
||||||
// Fog, Fadeout & Background
|
// Fog, Fadeout & Background
|
||||||
//
|
//
|
||||||
this.scene.background = new THREE.Color(0);
|
this.scene.background = new THREE.Color(0);
|
||||||
this.scene.fog = new THREE.Fog(0, 0, this.viewDistance - 1);
|
this.scene.fog = new THREE.Fog(0, 0, this.viewDistance);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Camera
|
// Camera
|
||||||
//
|
//
|
||||||
this.camera.up.set(0, 0, 1); // Z-up instead of Y-up
|
this.mainCamera.up.set(0, 0, 1); // Z-up instead of Y-up
|
||||||
|
|
||||||
//
|
//
|
||||||
// Torch
|
// Torch
|
||||||
//
|
//
|
||||||
this.torch = new THREE.PointLight(0xffffff, 0.9, this.viewDistance, 2); // https://threejs.org/docs/#api/en/lights/PointLight
|
this.torch = new THREE.PointLight(0xffffff, 2, this.viewDistance * 2, 1); // https://threejs.org/docs/#api/en/lights/PointLight
|
||||||
this.torch.position.copy(this.camera.position);
|
this.torch.position.copy(this.mainCamera.position);
|
||||||
this.scene.add(this.torch);
|
this.scene.add(this.torch);
|
||||||
|
|
||||||
|
this.textures = [];
|
||||||
|
|
||||||
|
for (const textureFile of textureFilenames) {
|
||||||
|
const tex = new THREE.TextureLoader().load(textureFile, (t) => {
|
||||||
|
t.magFilter = THREE.NearestFilter; // no smoothing when scaling up
|
||||||
|
t.minFilter = THREE.NearestFilter; // no mipmaps / no smoothing when scaling down
|
||||||
|
t.generateMipmaps = false; // don’t build mipmaps
|
||||||
|
});
|
||||||
|
this.textures.push(tex);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Sprites
|
// Sprites
|
||||||
//
|
//
|
||||||
@@ -65,8 +87,7 @@ export class FirstPersonRenderer {
|
|||||||
this.initMap();
|
this.initMap();
|
||||||
|
|
||||||
//
|
//
|
||||||
this.renderer.setSize(w, h);
|
this.renderer.setSize(this.asciiWidth * 1, this.asciiHeight * 1);
|
||||||
document.getElementById("threejs").appendChild(this.renderer.domElement);
|
|
||||||
this.renderFrame();
|
this.renderFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,15 +95,19 @@ export class FirstPersonRenderer {
|
|||||||
const wallPlanes = [];
|
const wallPlanes = [];
|
||||||
const sprites = [];
|
const sprites = [];
|
||||||
|
|
||||||
|
//
|
||||||
|
// -------------
|
||||||
|
// PARSE THE MAP
|
||||||
|
// -------------
|
||||||
/** @type {Map<number,Array} */
|
/** @type {Map<number,Array} */
|
||||||
this.map.forEach((/** @type {Tile} */ tile, /** @type {number} */ x, /** @type {number} y */ y) => {
|
this.map.forEach((/** @type {Tile} */ tile, /** @type {number} */ x, /** @type {number} y */ y) => {
|
||||||
//
|
//
|
||||||
if (tile.isStartLocation) {
|
if (tile.isStartLocation) {
|
||||||
this.camera.position.set(x, y, 0);
|
this.mainCamera.position.set(x, y, 0);
|
||||||
this.camera.lookAt(x, y - 1, 0);
|
this.mainCamera.lookAt(x, y - 1, 0);
|
||||||
this.torch.position.copy(this.camera.position);
|
this.torch.position.copy(this.mainCamera.position);
|
||||||
|
|
||||||
console.log("Initial Camera Position:", this.camera.position);
|
console.log("Initial Camera Position:", this.mainCamera.position);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +127,7 @@ export class FirstPersonRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tile.isSprite) {
|
if (tile.isEncounter) {
|
||||||
console.log("Sprite", tile);
|
console.log("Sprite", tile);
|
||||||
sprites.push([x, y, tile.textureId]);
|
sprites.push([x, y, tile.textureId]);
|
||||||
return;
|
return;
|
||||||
@@ -112,41 +137,48 @@ export class FirstPersonRenderer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//
|
//
|
||||||
// Floor (XY plane at Z = -.5)
|
// ---------------------------
|
||||||
//
|
// FLOOR (XY PLANE AT Z = -.5)
|
||||||
|
// ---------------------------
|
||||||
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({ color: 0x964b00 /* side: THREE.DoubleSide */ });
|
const floorMat = new THREE.MeshStandardMaterial({
|
||||||
|
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);
|
||||||
this.scene.add(floor);
|
this.scene.add(floor);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Ceiling (XY plane at Z = .5)
|
// -----------------------------
|
||||||
//
|
// CEILING (XY PLANE AT Z = .5)
|
||||||
|
// -----------------------------
|
||||||
const ceilingGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
|
const ceilingGeo = new THREE.PlaneGeometry(this.map.width, this.map.height);
|
||||||
const ceilingMat = new THREE.MeshStandardMaterial({ color: 0x333333, side: THREE.BackSide });
|
const ceilingMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: this.ceilingColor,
|
||||||
|
side: THREE.BackSide,
|
||||||
|
});
|
||||||
const ceiling = new THREE.Mesh(ceilingGeo, ceilingMat);
|
const ceiling = new THREE.Mesh(ceilingGeo, ceilingMat);
|
||||||
ceiling.position.set(this.map.width / 2, this.map.height / 2, 0.5);
|
ceiling.position.set(this.map.width / 2, this.map.height / 2, 0.5);
|
||||||
this.scene.add(ceiling);
|
this.scene.add(ceiling);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Walls
|
// ------
|
||||||
//
|
// 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();
|
const wallGeo = new THREE.PlaneGeometry();
|
||||||
wallGeo.rotateX(Math.PI / 2); // Get the geometry-plane the right way up (z-up)
|
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
|
wallGeo.rotateY(Math.PI); // rotate textures to be the right way up
|
||||||
|
|
||||||
const instancedMesh = new THREE.InstancedMesh(
|
const instancedMesh = new THREE.InstancedMesh(
|
||||||
wallGeo,
|
wallGeo,
|
||||||
new THREE.MeshStandardMaterial({ map: wallTex }),
|
new THREE.MeshStandardMaterial({ map: this.textures[0] }),
|
||||||
wallPlanes.length,
|
wallPlanes.length,
|
||||||
);
|
);
|
||||||
|
instancedMesh.userData.pastelMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
});
|
||||||
|
|
||||||
|
instancedMesh.userData.parimaryMaterial = instancedMesh.material;
|
||||||
this.scene.add(instancedMesh);
|
this.scene.add(instancedMesh);
|
||||||
|
|
||||||
// Temp objects for generating matrices
|
// Temp objects for generating matrices
|
||||||
@@ -162,29 +194,19 @@ export class FirstPersonRenderer {
|
|||||||
instancedMesh.instanceMatrix.needsUpdate = true;
|
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Sprites
|
// -------
|
||||||
|
// 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) {
|
for (const [x, y, textureId] of sprites) {
|
||||||
|
// TODO: only one material per sprite type
|
||||||
|
const spriteMat = new THREE.SpriteMaterial({
|
||||||
|
map: this.textures[textureId],
|
||||||
|
transparent: true,
|
||||||
|
});
|
||||||
const sprite = new THREE.Sprite(spriteMat);
|
const sprite = new THREE.Sprite(spriteMat);
|
||||||
sprite.position.set(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
0, // z (stand on floor)
|
|
||||||
);
|
|
||||||
sprite.position.set(x, y, 0);
|
sprite.position.set(x, y, 0);
|
||||||
|
sprite.userData.mapLocation = new Vector3(x, y, 0); // The location (in tilemap coordinates) of this sprite
|
||||||
this.sprites.push(sprite);
|
this.sprites.push(sprite);
|
||||||
this.scene.add(sprite);
|
this.scene.add(sprite);
|
||||||
console.log({ x, y, textureId });
|
console.log({ x, y, textureId });
|
||||||
@@ -192,23 +214,115 @@ export class FirstPersonRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderFrame(posX, posY, dirAngle, commit = true) {
|
renderFrame(posX, posY, dirAngle, commit = true) {
|
||||||
this.renderer.render(this.scene, this.camera);
|
//
|
||||||
const lookAtV = new THREE.Vector3(1, 0, 0);
|
const posV = new Vector3(posX, posY, 0);
|
||||||
lookAtV
|
|
||||||
.applyAxisAngle(new THREE.Vector3(0, 0, 1), dirAngle)
|
|
||||||
.normalize()
|
|
||||||
.add(this.camera.position);
|
|
||||||
|
|
||||||
this.camera.position.x = posX;
|
//
|
||||||
this.camera.position.y = posY;
|
// -------------------------------
|
||||||
|
// Camera Position and Orientation
|
||||||
|
// -------------------------------
|
||||||
|
//
|
||||||
|
// Direction we're looking
|
||||||
|
const lookDirV = new Vector3(1, 0, 0)
|
||||||
|
.applyAxisAngle(new Vector3(0, 0, 1), dirAngle)
|
||||||
|
.setZ(0)
|
||||||
|
.normalize();
|
||||||
|
|
||||||
this.torch.position.copy(this.camera.position);
|
//
|
||||||
this.torch.position.z += 0.25;
|
// The Point we're looking at.
|
||||||
this.camera.lookAt(lookAtV);
|
//
|
||||||
|
const lookAtV = lookDirV.clone().add(posV);
|
||||||
|
lookAtV.z = 0;
|
||||||
|
|
||||||
|
this.mainCamera.position.copy(posV); // Move the camera
|
||||||
|
this.mainCamera.lookAt(lookAtV); // Rotate the camera
|
||||||
|
|
||||||
|
// -----
|
||||||
|
// TORCH
|
||||||
|
// -----
|
||||||
|
//
|
||||||
|
// The torch should hover right above the camera
|
||||||
|
this.torch.position.set(posV.x, posV.y, posV.z + 0.25);
|
||||||
|
|
||||||
|
// -------
|
||||||
|
// SPRITES
|
||||||
|
// -------
|
||||||
|
//
|
||||||
|
this.sprites.forEach((sprite) => {
|
||||||
|
//
|
||||||
|
// The tilemap position (vector) of the sprite
|
||||||
|
/** @type {Vector3} */
|
||||||
|
const spriteCenterV = sprite.userData.mapLocation;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Direction from sprite to camera
|
||||||
|
const dir = new Vector3().subVectors(spriteCenterV, posV);
|
||||||
|
const len = dir.length();
|
||||||
|
|
||||||
|
//
|
||||||
|
if (len > this.viewDistance) {
|
||||||
|
// Sprite is out of range, do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(dir.x) > 1e-6 && Math.abs(dir.y) > 1e-6) {
|
||||||
|
// Sprite is not in a direct cardinal line to us, do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite.position.copy(spriteCenterV).addScaledVector(lookDirV, -0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
performance.mark("scene_render_start");
|
||||||
|
this.renderer.render(this.scene, this.mainCamera);
|
||||||
|
performance.mark("scene_render_end");
|
||||||
|
performance.measure("3D Scene Rendering", "scene_render_start", "scene_render_end");
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// ----------------
|
||||||
|
// ASCII Conversion
|
||||||
|
// ----------------
|
||||||
|
//
|
||||||
|
performance.mark("asciification_start");
|
||||||
|
const gl = this.renderer.getContext();
|
||||||
|
const width = this.renderer.domElement.width;
|
||||||
|
const height = this.renderer.domElement.height;
|
||||||
|
|
||||||
|
const pixels = new Uint8Array(width * height * 4);
|
||||||
|
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
for (let y = height - 1; y >= 0; y--) {
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
const r = pixels[idx];
|
||||||
|
const g = pixels[idx + 1];
|
||||||
|
const b = pixels[idx + 2];
|
||||||
|
|
||||||
|
const cssColor =
|
||||||
|
"#" + //
|
||||||
|
r.toString(16).padStart(2, "0") +
|
||||||
|
g.toString(16).padStart(2, "0") +
|
||||||
|
b.toString(16).padStart(2, "0");
|
||||||
|
|
||||||
|
this.window.put(x, y, "#", cssColor);
|
||||||
|
|
||||||
|
idx += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
performance.mark("asciification_end");
|
||||||
|
performance.measure(
|
||||||
|
"Asciification", // The name for our measurement
|
||||||
|
"asciification_start", // The starting mark
|
||||||
|
"asciification_end", // The ending mark
|
||||||
|
);
|
||||||
|
|
||||||
//
|
//
|
||||||
if (commit) {
|
if (commit) {
|
||||||
|
performance.mark("dom_commit_start");
|
||||||
this.window.commitToDOM();
|
this.window.commitToDOM();
|
||||||
|
performance.mark("dom_commit_end");
|
||||||
|
performance.measure("DOM Commit", "dom_commit_start", "dom_commit_end");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,129 +1,40 @@
|
|||||||
|
import { FunctionCallParser } from "../utils/callParser.js";
|
||||||
import { Vector2i, Orientation } from "./ascii_types.js";
|
import { Vector2i, Orientation } from "./ascii_types.js";
|
||||||
import { AsciiWindow } from "./ascii_window.js";
|
import { AsciiWindow } from "./ascii_window.js";
|
||||||
|
|
||||||
export class Tile {
|
|
||||||
/** @type {string} How should this tile be rendered on the minimap.*/
|
|
||||||
minimapChar = " ";
|
|
||||||
|
|
||||||
/** @type {string} How should this tile be rendered on the minimap.*/
|
|
||||||
minimapColor = "#fff";
|
|
||||||
|
|
||||||
/** @type {boolean} Should this be rendered as a wall? */
|
|
||||||
isWall = false;
|
|
||||||
|
|
||||||
/** @type {boolean} is this tile occupied by a sprite? */
|
|
||||||
isSprite = false;
|
|
||||||
|
|
||||||
/** @type {boolean} Can the player walk here? */
|
|
||||||
traversable = true;
|
|
||||||
|
|
||||||
/** @type {boolean} Is this where they player starts? */
|
|
||||||
isStartLocation = false;
|
|
||||||
|
|
||||||
/** @type {boolean} Is this where they player starts? */
|
|
||||||
textureId = 0;
|
|
||||||
|
|
||||||
/** @type {Tile} options */
|
|
||||||
constructor(options) {
|
|
||||||
for (let [k, v] of Object.entries(options)) {
|
|
||||||
if (this[k] !== undefined) {
|
|
||||||
this[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get collision() {
|
|
||||||
return this.isWall || this.isSprite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultLegend = Object.freeze({
|
|
||||||
//
|
|
||||||
// "" is the Unknown Tile - if we encounter a tile that we don't know how to parse,
|
|
||||||
// the it will be noted here as the empty string
|
|
||||||
"": new Tile({
|
|
||||||
minimapChar: " ",
|
|
||||||
traversable: true,
|
|
||||||
isWall: false,
|
|
||||||
}),
|
|
||||||
|
|
||||||
//
|
|
||||||
// default floor
|
|
||||||
" ": new Tile({
|
|
||||||
minimapChar: " ",
|
|
||||||
traversable: true,
|
|
||||||
isWall: false,
|
|
||||||
}),
|
|
||||||
//
|
|
||||||
// Default wall
|
|
||||||
"#": new Tile({
|
|
||||||
minimapChar: "#",
|
|
||||||
traversable: false,
|
|
||||||
isWall: true,
|
|
||||||
textureId: 0,
|
|
||||||
}),
|
|
||||||
|
|
||||||
"M": new Tile({
|
|
||||||
textureId: 1,
|
|
||||||
minimapChar: "M",
|
|
||||||
minimapColor: "#f00",
|
|
||||||
traversable: false,
|
|
||||||
isWall: false,
|
|
||||||
isSprite: true,
|
|
||||||
}),
|
|
||||||
|
|
||||||
//
|
|
||||||
//secret door (looks like wall, but is traversable)
|
|
||||||
"Ω": new Tile({
|
|
||||||
minimapChar: "#",
|
|
||||||
traversable: true,
|
|
||||||
isWall: true,
|
|
||||||
}),
|
|
||||||
//
|
|
||||||
// where the player starts
|
|
||||||
"S": new Tile({
|
|
||||||
minimapChar: "S", // "Š",
|
|
||||||
traversable: true,
|
|
||||||
isWall: false,
|
|
||||||
isStartLocation: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export class TileMap {
|
export class TileMap {
|
||||||
/**
|
/**
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
* @param {Record<string,Tile} legend
|
* @param {Record<string,Tile} legend
|
||||||
*/
|
*/
|
||||||
static fromText(str, legend = defaultLegend) {
|
static fromText(str) {
|
||||||
const lines = str.split("\n");
|
const lines = str.split("\n");
|
||||||
|
const tiles = [];
|
||||||
|
const options = [];
|
||||||
|
const optionsParser = new FunctionCallParser();
|
||||||
|
|
||||||
const longestLine = lines.reduce((acc, line) => Math.max(acc, line.length), 0);
|
let mapWidth;
|
||||||
|
|
||||||
const tiles = new Array(lines.length).fill().map(() => Array(longestLine));
|
|
||||||
|
|
||||||
lines.forEach((line, y) => {
|
lines.forEach((line, y) => {
|
||||||
line = line.padEnd(longestLine, "#");
|
tiles[y] = [];
|
||||||
|
options[y] = [];
|
||||||
|
|
||||||
line.split("").forEach((char, x) => {
|
// Everything before ":::" is map tiles, and everything after is options for the tiles on that line
|
||||||
let tile = legend[char];
|
let [tileStr, optionStr] = line.split(/\s*:::\s*/);
|
||||||
|
|
||||||
// unknown char?
|
// Infer the width of the map from the first line
|
||||||
// check fallback tile.
|
if (!mapWidth) {
|
||||||
if (tile === undefined) {
|
mapWidth = tileStr.length;
|
||||||
tile = legend[""];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// still no tile - i.e. no back fallback tile?
|
optionStr = optionStr.split(/\s*\/\//)[0];
|
||||||
if (tile === undefined) {
|
options[y] = optionStr ? optionsParser.parse(optionStr) : [];
|
||||||
throw new Error("Dont know how to handle this character: " + char);
|
|
||||||
}
|
|
||||||
|
|
||||||
// insert tile into map.
|
// STFU Linda
|
||||||
tiles[y][x] = tile;
|
console.log(tileStr, optionStr, y);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return new TileMap(longestLine, lines.length, tiles);
|
// return new TileMap(longestLine, lines.length, tiles, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
tileIdx(x, y) {
|
tileIdx(x, y) {
|
||||||
@@ -184,9 +95,25 @@ export class TileMap {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.tiles[y][x]) {
|
||||||
|
x++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return this.tiles[y][x].isWall;
|
return this.tiles[y][x].isWall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTraversable(x, y) {
|
||||||
|
x |= 0;
|
||||||
|
y |= 0;
|
||||||
|
|
||||||
|
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.tiles[y][x].isTraversable;
|
||||||
|
}
|
||||||
|
|
||||||
findFirst(criteria) {
|
findFirst(criteria) {
|
||||||
return this.forEach((tile, x, y) => {
|
return this.forEach((tile, x, y) => {
|
||||||
for (let k in criteria) {
|
for (let k in criteria) {
|
||||||
|
|||||||
147
frontend/ascii_tile_types.js
Executable file
147
frontend/ascii_tile_types.js
Executable file
@@ -0,0 +1,147 @@
|
|||||||
|
import { Orientation } from "./ascii_types";
|
||||||
|
|
||||||
|
export class Tile {
|
||||||
|
/** @type {string} How should this tile be rendered on the minimap.*/
|
||||||
|
minimapChar;
|
||||||
|
/** @type {string} How should this tile be rendered on the minimap.*/
|
||||||
|
minimapColor;
|
||||||
|
/** @type {boolean} Should this be rendered as a wall? */
|
||||||
|
isWall;
|
||||||
|
/** @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} Is this a two-way portal entry/exit */
|
||||||
|
isTwoWayPortal;
|
||||||
|
/** @type {boolean} Is this a one-way portal entry */
|
||||||
|
isOneWayPortalEntry;
|
||||||
|
/** @type {boolean} Is this a one-way portal exit */
|
||||||
|
isOneWayPortalExit;
|
||||||
|
/** @type {boolean} Has the secret properties of this tile been uncovered? */
|
||||||
|
isUncovered;
|
||||||
|
/** @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 */
|
||||||
|
textureId;
|
||||||
|
/** @type {number|string} id the encounter located on this tile */
|
||||||
|
encounterId;
|
||||||
|
/** @type {boolean} Can/does this tile wander around on empty tiles? */
|
||||||
|
isWandering;
|
||||||
|
/** @type {Orientation} */
|
||||||
|
orientation;
|
||||||
|
|
||||||
|
/** @param {Tile} options */
|
||||||
|
constructor(options) {
|
||||||
|
for (let [k, v] of Object.entries(options)) {
|
||||||
|
if (this[k] !== undefined) {
|
||||||
|
this[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Tile} options */
|
||||||
|
static fromChar(char, options = {}) {
|
||||||
|
switch (char) {
|
||||||
|
case " ":
|
||||||
|
return new FloorTile();
|
||||||
|
case "#":
|
||||||
|
return new WallTile();
|
||||||
|
case "P":
|
||||||
|
return new PlayerStartTile(options.orientation);
|
||||||
|
case "E":
|
||||||
|
return new EncounterTile(options.textureId, options.encounterId);
|
||||||
|
case "O":
|
||||||
|
return new SecretOneWayPortalEntryTile(options.channel);
|
||||||
|
case "o":
|
||||||
|
return new SecretOneWayPortalExitTile(options.channel);
|
||||||
|
case "Z":
|
||||||
|
return new SecretTwoWayPortalTile(options.channel);
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown character: " + char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new this.constructor(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FloorTile extends Tile {
|
||||||
|
isTraversable = true;
|
||||||
|
minimapChar = " ";
|
||||||
|
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 = 0;
|
||||||
|
isTraversable = false;
|
||||||
|
isWall = true;
|
||||||
|
minimapChar = "#";
|
||||||
|
internalMapChar = "#";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EncounterTile extends Tile {
|
||||||
|
isEncounter = true;
|
||||||
|
constructor(textureId, encounterId) {
|
||||||
|
super({ textureId, encounterId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-way portal entries look exactly like walls. You need to
|
||||||
|
* probe for them, or otherwise unlock their location.
|
||||||
|
* You can walk into them, and then the magic happens
|
||||||
|
*/
|
||||||
|
export class SecretOneWayPortalEntryTile extends Tile {
|
||||||
|
textureId = 0;
|
||||||
|
isWall = true;
|
||||||
|
isTraversable = true; // we can walk in to it?
|
||||||
|
isOneWayPortalEntry = true;
|
||||||
|
internalMapChar = "O";
|
||||||
|
minimapChar = "#"; // Change char when the portal has been uncovered
|
||||||
|
isUncovered = false;
|
||||||
|
|
||||||
|
constructor(channel) {
|
||||||
|
super({ channel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecretOneWayPortalExitTile extends Tile {
|
||||||
|
isTraversable = true;
|
||||||
|
isOneWayPortalExit = true;
|
||||||
|
internalMapChar = "o";
|
||||||
|
minimapChar = " "; // Change char when the portal has been uncovered
|
||||||
|
isUncovered = false;
|
||||||
|
|
||||||
|
constructor(channel) {
|
||||||
|
super({ channel });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecretTwoWayPortalTile extends Tile {
|
||||||
|
textureId = 0;
|
||||||
|
isWall = true;
|
||||||
|
isTraversable = true;
|
||||||
|
isTwoWayPortalEntry = true;
|
||||||
|
internalMapChar = "0";
|
||||||
|
minimapChar = "#"; // Change char when the portal has been uncovered
|
||||||
|
isUncovered = false;
|
||||||
|
|
||||||
|
constructor(channel) {
|
||||||
|
super({ channel });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@ export const PI_OVER_TWO = Math.PI / 2;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum Cardinal Direction (east north west south)
|
* Enum Cardinal Direction (east north west south)
|
||||||
* @constant
|
* @constant @readonly @enum {number}
|
||||||
* @readonly
|
|
||||||
*/
|
*/
|
||||||
export const Orientation = {
|
export const Orientation = {
|
||||||
/** @constant @readonly @type {number} Going east increases X */
|
/** @constant @readonly @type {number} Going east increases X */
|
||||||
@@ -18,7 +17,7 @@ export const Orientation = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum Relative Direction (forward, left, right, backwards)
|
* Enum Relative Direction (forward, left, right, backwards)
|
||||||
* @readonly
|
* @constant @readonly @enum {number}
|
||||||
*/
|
*/
|
||||||
export const RelativeMovement = {
|
export const RelativeMovement = {
|
||||||
FORWARD: 0,
|
FORWARD: 0,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
0
frontend/eob1.png → frontend/eobBlueWall.png
Normal file → Executable file
0
frontend/eob1.png → frontend/eobBlueWall.png
Normal file → Executable file
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
frontend/eobRedWall.png
Normal file
BIN
frontend/eobRedWall.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 16 KiB |
26
node_modules/.vite/deps/_metadata.json
generated
vendored
26
node_modules/.vite/deps/_metadata.json
generated
vendored
@@ -2,28 +2,46 @@
|
|||||||
"hash": "5eac6a41",
|
"hash": "5eac6a41",
|
||||||
"configHash": "86a557ed",
|
"configHash": "86a557ed",
|
||||||
"lockfileHash": "3ceab950",
|
"lockfileHash": "3ceab950",
|
||||||
"browserHash": "20105502",
|
"browserHash": "1d3df51c",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"sprintf-js": {
|
"sprintf-js": {
|
||||||
"src": "../../sprintf-js/src/sprintf.js",
|
"src": "../../sprintf-js/src/sprintf.js",
|
||||||
"file": "sprintf-js.js",
|
"file": "sprintf-js.js",
|
||||||
"fileHash": "089e5f0c",
|
"fileHash": "cfe4c24f",
|
||||||
"needsInterop": true
|
"needsInterop": true
|
||||||
},
|
},
|
||||||
"three": {
|
"three": {
|
||||||
"src": "../../three/build/three.module.js",
|
"src": "../../three/build/three.module.js",
|
||||||
"file": "three.js",
|
"file": "three.js",
|
||||||
"fileHash": "22510eaa",
|
"fileHash": "7e792144",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"three/src/math/MathUtils.js": {
|
"three/src/math/MathUtils.js": {
|
||||||
"src": "../../three/src/math/MathUtils.js",
|
"src": "../../three/src/math/MathUtils.js",
|
||||||
"file": "three_src_math_MathUtils__js.js",
|
"file": "three_src_math_MathUtils__js.js",
|
||||||
"fileHash": "f611651c",
|
"fileHash": "6afcef1b",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"three/tsl": {
|
||||||
|
"src": "../../three/build/three.tsl.js",
|
||||||
|
"file": "three_tsl.js",
|
||||||
|
"fileHash": "40fd901e",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"three/webgpu": {
|
||||||
|
"src": "../../three/build/three.webgpu.js",
|
||||||
|
"file": "three_webgpu.js",
|
||||||
|
"fileHash": "0d6a1d7c",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chunks": {
|
"chunks": {
|
||||||
|
"chunk-5FFPRNLG": {
|
||||||
|
"file": "chunk-5FFPRNLG.js"
|
||||||
|
},
|
||||||
|
"chunk-GHUIN7QU": {
|
||||||
|
"file": "chunk-GHUIN7QU.js"
|
||||||
|
},
|
||||||
"chunk-BUSYA2B4": {
|
"chunk-BUSYA2B4": {
|
||||||
"file": "chunk-BUSYA2B4.js"
|
"file": "chunk-BUSYA2B4.js"
|
||||||
}
|
}
|
||||||
|
|||||||
97
utils/callParser.js
Executable file
97
utils/callParser.js
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
export class FunctionCallParser {
|
||||||
|
/**
|
||||||
|
* @typedef {{name: string, args: Array<number|string|boolean|null>}} CallType
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} input
|
||||||
|
*
|
||||||
|
* @returns {CallType[]}
|
||||||
|
*/
|
||||||
|
parse(input) {
|
||||||
|
const calls = [];
|
||||||
|
const pattern = /(\w+)\s*\(([^)]*)\)/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = pattern.exec(input)) !== null) {
|
||||||
|
const name = match[1];
|
||||||
|
const argsStr = match[2].trim();
|
||||||
|
const args = this.parseArguments(argsStr);
|
||||||
|
|
||||||
|
calls.push({ name, args });
|
||||||
|
}
|
||||||
|
|
||||||
|
return calls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
|
parseArguments(argsStr) {
|
||||||
|
if (!argsStr) return [];
|
||||||
|
|
||||||
|
const args = [];
|
||||||
|
const tokens = this.tokenize(argsStr);
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
args.push(this.parseValue(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
|
tokenize(argsStr) {
|
||||||
|
const tokens = [];
|
||||||
|
let current = "";
|
||||||
|
let depth = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < argsStr.length; i++) {
|
||||||
|
const char = argsStr[i];
|
||||||
|
|
||||||
|
if (char === "(" || char === "[" || char === "{") {
|
||||||
|
depth++;
|
||||||
|
current += char;
|
||||||
|
} else if (char === ")" || char === "]" || char === "}") {
|
||||||
|
depth--;
|
||||||
|
current += char;
|
||||||
|
} else if (char === "," && depth === 0) {
|
||||||
|
if (current.trim()) {
|
||||||
|
tokens.push(current.trim());
|
||||||
|
}
|
||||||
|
current = "";
|
||||||
|
} else {
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.trim()) {
|
||||||
|
tokens.push(current.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @protected */
|
||||||
|
parseValue(str) {
|
||||||
|
str = str.trim();
|
||||||
|
|
||||||
|
// Try to parse as number
|
||||||
|
if (/^-?\d+(\.\d+)?$/.test(str)) {
|
||||||
|
return parseFloat(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boolean
|
||||||
|
if (str === "true") return true;
|
||||||
|
if (str === "false") return false;
|
||||||
|
|
||||||
|
// Null/undefined
|
||||||
|
if (str === "null") return null;
|
||||||
|
|
||||||
|
// Otherwise treat as string (remove quotes if present)
|
||||||
|
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
|
||||||
|
return str.slice(1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user