Whambap
This commit is contained in:
41
bench.js
Executable file
41
bench.js
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
function dataset() {
|
||||||
|
return new Array(4000).fill().map(() => "fusse".repeat(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
const simCount = 100_000;
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
const target = "not found";
|
||||||
|
|
||||||
|
for (let sim = 0; sim < simCount; sim++) {
|
||||||
|
const ds = dataset();
|
||||||
|
|
||||||
|
// const len = ds.length;
|
||||||
|
// for (let i = 0; i < len; i++) {
|
||||||
|
// // pretend to do work on the data elements
|
||||||
|
// const el = ds[i];
|
||||||
|
// if (el === target) {
|
||||||
|
// console.log("foo");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ds.forEach((el) => {
|
||||||
|
// if (el === target) {
|
||||||
|
// console.log("foo");
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
while (ds.length > 0) {
|
||||||
|
let el = ds.pop();
|
||||||
|
if (el === target) {
|
||||||
|
console.log("foo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("time: %f msec", Date.now() - start);
|
||||||
|
|
||||||
|
// for-loop : 8568 msec
|
||||||
|
// .forEach : 8551 msec
|
||||||
|
// pop() : 8765 msec
|
||||||
@@ -5,30 +5,6 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ASCII Dungeon Crawler</title>
|
<title>ASCII Dungeon Crawler</title>
|
||||||
<style>
|
<style>
|
||||||
#color-lens {
|
|
||||||
color: yellow;
|
|
||||||
font-size: 40px;
|
|
||||||
/* Shape and Color */
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
background-color: none;
|
|
||||||
border-radius: 50%; /* Makes it a circle */
|
|
||||||
|
|
||||||
/* Positioning and Layering */
|
|
||||||
position: fixed; /* Floats on top of the page */
|
|
||||||
z-index: 9999;
|
|
||||||
|
|
||||||
/* Make it invisible to the cursor */
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
/* The color blending effect! */
|
|
||||||
mix-blend-mode: color; /* try 'hue' or 'color' for different effects */
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
/* Initially hide it and center it on the cursor */
|
|
||||||
display: none;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -42,9 +18,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#viewport {
|
#viewport,
|
||||||
font-size: 8px;
|
#minimap {
|
||||||
line-height: 8px;
|
font-size: 10px;
|
||||||
|
line-height: 10px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
border: 2px solid #0f0;
|
border: 2px solid #0f0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -62,6 +39,60 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#minimap .player {
|
||||||
|
position: relative; /* anchor */
|
||||||
|
color: #000; /* text blends into background */
|
||||||
|
/* background-color: red; */
|
||||||
|
}
|
||||||
|
#minimap .player.north::before {
|
||||||
|
content: "↑";
|
||||||
|
/* content: "★"; */
|
||||||
|
position: absolute;
|
||||||
|
inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */
|
||||||
|
display: flex; /* center it */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none; /* don’t block clicks */
|
||||||
|
font-size: 1.5em; /* bigger if you want */
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
#minimap .player.south::before {
|
||||||
|
content: "↓";
|
||||||
|
/* content: "★"; */
|
||||||
|
position: absolute;
|
||||||
|
inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */
|
||||||
|
display: flex; /* center it */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none; /* don’t block clicks */
|
||||||
|
font-size: 1.5em; /* bigger if you want */
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
#minimap .player.east::before {
|
||||||
|
content: "→";
|
||||||
|
/* content: "★"; */
|
||||||
|
position: absolute;
|
||||||
|
inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */
|
||||||
|
display: flex; /* center it */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none; /* don’t block clicks */
|
||||||
|
font-size: 1.5em; /* bigger if you want */
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
#minimap .player.west::before {
|
||||||
|
content: "←";
|
||||||
|
/* content: "★"; */
|
||||||
|
position: absolute;
|
||||||
|
inset: 0; /* shorthand for top:0; right:0; bottom:0; left:0; */
|
||||||
|
display: flex; /* center it */
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none; /* don’t block clicks */
|
||||||
|
font-size: 1.5em; /* bigger if you want */
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
background-color: #001100;
|
background-color: #001100;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
@@ -87,29 +118,59 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="gameContainer">
|
<div id="gameContainer">
|
||||||
<div id="viewport"></div>
|
<div id="viewport"></div>
|
||||||
<div id="controls">
|
|
||||||
<div>Use WASD or Arrow Keys to move and arrow keys to turn</div>
|
<div id="minimap"></div>
|
||||||
</div>
|
|
||||||
|
<div id="compass">orientation</div>
|
||||||
|
|
||||||
<div id="mapInput">
|
<div id="mapInput">
|
||||||
<div>Load your map (# = walls, space = floor):</div>
|
<div>Load your map (# = walls, space = floor):</div>
|
||||||
<br />
|
<br />
|
||||||
<textarea id="mapText" rows="10" cols="50">
|
<textarea id="mapText" rows="10" cols="50">
|
||||||
####################
|
############################################################
|
||||||
# # #
|
############################################################
|
||||||
# # ### #
|
############################################################
|
||||||
# # # #
|
## ################# ########################
|
||||||
# #### # #
|
## # # ################# # ## ########################
|
||||||
# # #
|
## # ################# # ## ################
|
||||||
# # # #
|
## # # ################# # ## #### ####
|
||||||
# # #### # #
|
## # # ## # #### # # ####
|
||||||
# # # #
|
###### #################### ## #### # ####
|
||||||
# ### ### #
|
###### #################### # ## # # #### ####
|
||||||
# #
|
###### #################### # ####
|
||||||
####################
|
###### #################### ########## #### ####
|
||||||
|
###### #################### ########## # # #### # # ####
|
||||||
|
###### #################### ########## #### # # ####
|
||||||
|
###### #################### #################### ####
|
||||||
|
###### #################### ############################
|
||||||
|
###### #################### # ############################
|
||||||
|
###### #################### # ############################
|
||||||
|
###### #################### # ############################
|
||||||
|
###### ## ########### ############################
|
||||||
|
###### ## ########### # # ############################
|
||||||
|
###### ## ########### ######## ############
|
||||||
|
###### ## ## # ########### ## ######## ############
|
||||||
|
###### ## # # ############
|
||||||
|
###### # # # ############
|
||||||
|
######### # ## ########### # ######### # ############
|
||||||
|
######### # # ########### # ######### # # ############
|
||||||
|
######### ########### # ######### ############
|
||||||
|
########################### # ######### #### ###############
|
||||||
|
########################### # ######### #### ###############
|
||||||
|
########################### # ######### #### ###############
|
||||||
|
########################### # ######### #### ###############
|
||||||
|
########################### # #### ###############
|
||||||
|
######################### # #### # # # ######## ###
|
||||||
|
######################### # # ######## # ###
|
||||||
|
######################### # ##### # # # # ######## ###
|
||||||
|
######################### # # ###
|
||||||
|
######################### ####################### # ###
|
||||||
|
##################################################### ###
|
||||||
|
############################################################
|
||||||
</textarea
|
</textarea
|
||||||
>
|
>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<button onclick="loadMap()">Load Map</button>
|
<button onclick="game.loadMap()">Load Map</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
536
frontend/ascii_dungeon_crawler.js
Executable file
536
frontend/ascii_dungeon_crawler.js
Executable file
@@ -0,0 +1,536 @@
|
|||||||
|
import { sprintf } from "sprintf-js";
|
||||||
|
import { Vector2i } from "./vec2.js";
|
||||||
|
import { AsciiWindow } from "./ascrii_window.js";
|
||||||
|
|
||||||
|
const PI_OVER_TWO = Math.PI / 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum Cardinal Direction (east north west south)
|
||||||
|
* @constant
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
const CardinalDirection = {
|
||||||
|
/** @constant @readonly @type {number} Going east increases X */
|
||||||
|
EAST: 0,
|
||||||
|
/** @constant @readonly @type {number} Going south increases Y */
|
||||||
|
SOUTH: 1,
|
||||||
|
/** @constant @readonly @type {number} Going west decreases X */
|
||||||
|
WEST: 2,
|
||||||
|
/** @constant @readonly @type {number} Going south decreases Y */
|
||||||
|
NORTH: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum Relative Direction (forward, left, right, backwards)
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
const RelativeMovement = {
|
||||||
|
FORWARD: 0,
|
||||||
|
LEFT: 3,
|
||||||
|
BACKWARD: 2,
|
||||||
|
RIGHT: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Player {
|
||||||
|
_posV = new Vector2i();
|
||||||
|
_directionV = new Vector2i(0, 1);
|
||||||
|
|
||||||
|
get x() {
|
||||||
|
return this._posV.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
get y() {
|
||||||
|
return this._posV.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
set x(x) {
|
||||||
|
this._posV.x = x | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set y(y) {
|
||||||
|
this._posV.y = y | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get angle() {
|
||||||
|
return this._directionV.angle();
|
||||||
|
}
|
||||||
|
|
||||||
|
get orientation() {
|
||||||
|
return this._directionV.cardinalDirection();
|
||||||
|
}
|
||||||
|
|
||||||
|
set orientation(o) {
|
||||||
|
//
|
||||||
|
// Sanitize o
|
||||||
|
o = ((o | 0) + 4) % 4;
|
||||||
|
|
||||||
|
if (o === CardinalDirection.EAST) {
|
||||||
|
this._directionV = new Vector2i(1, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (o === CardinalDirection.NORTH) {
|
||||||
|
this._directionV = new Vector2i(0, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (o === CardinalDirection.WEST) {
|
||||||
|
this._directionV = new Vector2i(-1, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (o === CardinalDirection.SOUTH) {
|
||||||
|
this._directionV = new Vector2i(0, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withinAABB(maxX, maxY, minX = 0, minY = 0) {
|
||||||
|
return this._posV.x >= minX && this._posV.x <= maxX && this._posV.y >= minY && this._posV.y <= maxY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DungeonCrawler {
|
||||||
|
get isAnimating() {
|
||||||
|
return this.animation.frames.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
/** @type {number} Number of times per second we poll for controller inputs */
|
||||||
|
this.pollsPerSec = 60;
|
||||||
|
/** @type {number} */
|
||||||
|
this.debounce = 0;
|
||||||
|
|
||||||
|
/** @constant @readonly */
|
||||||
|
this.keys = {
|
||||||
|
/** @constant @readonly */
|
||||||
|
handlers: {},
|
||||||
|
/** @constant @readonly */
|
||||||
|
pressed: {},
|
||||||
|
/** @constant @readonly */
|
||||||
|
names: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.map = {
|
||||||
|
/** @readonly height of map */
|
||||||
|
width: 0,
|
||||||
|
|
||||||
|
/** @readonly width of map */
|
||||||
|
height: 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @type {Uint8Array[]}
|
||||||
|
* info about each cell/tile in the map (is it floor, is it a wall, etc.)
|
||||||
|
*
|
||||||
|
* The number 0 is navigable and has no decoration.
|
||||||
|
* The number 1 is not navigable, and is a nondescript wall.
|
||||||
|
*
|
||||||
|
* 1 bit for navigable
|
||||||
|
* 3 bits for cell type decoration / voxel type
|
||||||
|
* 2 bits for floor decoration.
|
||||||
|
* 2 bits for ceiling decoration.
|
||||||
|
*/
|
||||||
|
cells: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animation = {
|
||||||
|
/** @constant @readonly @type {number} Number of frames per second used in animations */
|
||||||
|
fps: 30,
|
||||||
|
|
||||||
|
/** @constant @readonly number of seconds a typical animation takes */
|
||||||
|
duration: 0.7,
|
||||||
|
|
||||||
|
/** Array storing information about each frame of an animation to show */
|
||||||
|
frames: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @readonly */
|
||||||
|
this.rendering = {
|
||||||
|
enabled: true,
|
||||||
|
ticker: 0,
|
||||||
|
maxDepth: 5,
|
||||||
|
fov: Math.PI / 3, // 60 degrees, increase maybe?
|
||||||
|
view: new AsciiWindow(document.getElementById("viewport"), 120, 40),
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @readonly */
|
||||||
|
this.minimap = {
|
||||||
|
parentElement: document.getElementById("minimap"),
|
||||||
|
/** @type {Element[][]} */
|
||||||
|
elements: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Player
|
||||||
|
* @type {object}
|
||||||
|
* @property {number} x integer. Player's x-coordinate on the grid.
|
||||||
|
* @property {number} y integer. Player's y-coordinate on the grid.
|
||||||
|
*/
|
||||||
|
this.player = new Player();
|
||||||
|
|
||||||
|
this.setupControls();
|
||||||
|
this.setupAnimationLoop();
|
||||||
|
|
||||||
|
this.loadMap();
|
||||||
|
this.render(this.player.x, this.player.y, this.player.orientation * PI_OVER_TWO, this.animation.frames.length);
|
||||||
|
this.updateCompass();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMap() {
|
||||||
|
this.minimap.parentElement.innerHTML = "";
|
||||||
|
|
||||||
|
const mapString = document.getElementById("mapText").value;
|
||||||
|
const lines = mapString.trim().split("\n");
|
||||||
|
|
||||||
|
const h = (this.map.height = lines.length);
|
||||||
|
const w = (this.map.width = Math.max(...lines.map((line) => line.length)));
|
||||||
|
|
||||||
|
this.map.cells = new Array(h).fill().map(() => new Uint8Array(w));
|
||||||
|
this.minimap.elements = new Array(h).fill().map(() => new Array(w));
|
||||||
|
this.minimap.parentElement.innerHTML = "";
|
||||||
|
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
//
|
||||||
|
const row = document.createElement("div");
|
||||||
|
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const isFree = lines[y][x] === " ";
|
||||||
|
|
||||||
|
//
|
||||||
|
// === Internal map ===
|
||||||
|
//
|
||||||
|
this.map.cells[y][x] = isFree ? 0 : 1;
|
||||||
|
|
||||||
|
//
|
||||||
|
// === Mini Map ===
|
||||||
|
//
|
||||||
|
const mmElement = document.createElement("span");
|
||||||
|
mmElement.textContent = isFree ? " " : "#";
|
||||||
|
row.appendChild(mmElement);
|
||||||
|
this.minimap.elements[y][x] = mmElement;
|
||||||
|
}
|
||||||
|
this.minimap.parentElement.appendChild(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a starting position (first open space)
|
||||||
|
for (let y = 1; y < this.map.height - 1; y++) {
|
||||||
|
for (let x = 1; x < this.map.width - 1; x++) {
|
||||||
|
if (this.isWall(x, y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.player.x = x;
|
||||||
|
this.player.y = y;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateMinimap();
|
||||||
|
}
|
||||||
|
|
||||||
|
startTurnAnimation(clockwise, quarterTurns = 1) {
|
||||||
|
if (this.isAnimating) {
|
||||||
|
throw new Error("Cannot start an animation while one is already running");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarterTurns === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDirection = clockwise
|
||||||
|
? this.player._directionV.clone().rotateCW(quarterTurns)
|
||||||
|
: this.player._directionV.clone().rotateCCW(quarterTurns);
|
||||||
|
|
||||||
|
const ticks = Math.floor(this.animation.duration * this.animation.fps);
|
||||||
|
const startAngle = this.player.angle;
|
||||||
|
const slice = this.player._directionV.angleTo(newDirection) / ticks;
|
||||||
|
|
||||||
|
this.animation.frames = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < ticks; i++) {
|
||||||
|
this.animation.frames.push([
|
||||||
|
this.player.x, //
|
||||||
|
this.player.y, //
|
||||||
|
startAngle + slice * i, //
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animation.frames.push([this.player.x, this.player.y, newDirection.angle()]);
|
||||||
|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
this.player._directionV = newDirection;
|
||||||
|
this.updateMinimap();
|
||||||
|
this.updateCompass();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {RelativeMovement} Direction the player is going to move */
|
||||||
|
startMoveAnimation(direction) {
|
||||||
|
if (this.isAnimating) {
|
||||||
|
throw new Error("Cannot start an animation while one is already running");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetV = this.player._directionV.rotatedCCW(direction | 0).added(this.player._posV);
|
||||||
|
|
||||||
|
if (this.isWall(targetV.x, targetV.y)) {
|
||||||
|
this.debounce = (this.pollsPerSec / 5) | 0;
|
||||||
|
console.info(
|
||||||
|
"bumped into wall at %s (mypos: %s), direction=%d",
|
||||||
|
targetV,
|
||||||
|
this.player._posV,
|
||||||
|
this.player.angle,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ticks = Math.floor(this.animation.duration * this.animation.fps);
|
||||||
|
const stepX = (targetV.x - this.player.x) / ticks;
|
||||||
|
const stepY = (targetV.y - this.player.y) / ticks;
|
||||||
|
|
||||||
|
this.animation.frames = [];
|
||||||
|
for (let i = 1; i < ticks; i++) {
|
||||||
|
this.animation.frames.push([
|
||||||
|
this.player.x + stepX * i, //
|
||||||
|
this.player.y + stepY * i, //
|
||||||
|
this.player.angle, //
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
this.animation.frames.push([targetV.x, targetV.y, this.player.angle]);
|
||||||
|
this.player._posV = targetV;
|
||||||
|
this.updateMinimap();
|
||||||
|
this.updateCompass(); // technically not necessary, but Im anticipating the need + compensating for my bad memory.
|
||||||
|
}
|
||||||
|
|
||||||
|
setupControls() {
|
||||||
|
this.keys.pressed = {};
|
||||||
|
|
||||||
|
this.keys.handlers = {
|
||||||
|
KeyA: () => this.startMoveAnimation(RelativeMovement.LEFT),
|
||||||
|
KeyD: () => this.startMoveAnimation(RelativeMovement.RIGHT),
|
||||||
|
KeyS: () => this.startMoveAnimation(RelativeMovement.BACKWARD),
|
||||||
|
KeyW: () => this.startMoveAnimation(RelativeMovement.FORWARD),
|
||||||
|
ArrowUp: () => this.startMoveAnimation(RelativeMovement.FORWARD),
|
||||||
|
ArrowDown: () => this.startMoveAnimation(RelativeMovement.BACKWARD),
|
||||||
|
ArrowLeft: () => this.startTurnAnimation(true),
|
||||||
|
ArrowRight: () => this.startTurnAnimation(false),
|
||||||
|
KeyQ: () => this.startTurnAnimation(true),
|
||||||
|
KeyE: () => this.startTurnAnimation(false),
|
||||||
|
};
|
||||||
|
this.keys.names = Object.keys(this.keys.handlers);
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
(e) => {
|
||||||
|
const id = e.code;
|
||||||
|
this.keys.pressed[id] = true;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener(
|
||||||
|
"keyup",
|
||||||
|
(e) => {
|
||||||
|
const id = e.code;
|
||||||
|
this.keys.pressed[id] = false;
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ticks = Math.round(1000 / this.pollsPerSec);
|
||||||
|
this.keys.interval = setInterval(() => {
|
||||||
|
this.handleKeyboardInput();
|
||||||
|
}, ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyboardInput() {
|
||||||
|
if (this.debounce > 0) {
|
||||||
|
this.debounce--;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Check each key we can handle.
|
||||||
|
for (let key of this.keys.names) {
|
||||||
|
if (this.keys.pressed[key]) {
|
||||||
|
this.debounce = Math.floor(this.animation.fps * this.animation.animationDuration) - 1;
|
||||||
|
const keyHandler = this.keys.handlers[key];
|
||||||
|
keyHandler();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAnimation() {
|
||||||
|
if (!this.isAnimating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y, a] = this.animation.frames.shift();
|
||||||
|
const framesLeft = this.animation.frames.length;
|
||||||
|
this.render(x, y, a, framesLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
// _____ ___ ____ ___
|
||||||
|
// |_ _/ _ \| _ \ / _ \ _
|
||||||
|
// | || | | | | | | | | (_)
|
||||||
|
// | || |_| | |_| | |_| |_
|
||||||
|
// |_| \___/|____/ \___/(_)
|
||||||
|
// -----------------------------
|
||||||
|
// Animation loop
|
||||||
|
// requestAnimationFrame(loop);
|
||||||
|
// requires using deltaT rather than ticks, etc.
|
||||||
|
setupAnimationLoop() {
|
||||||
|
const ticks = Math.round(1000 / this.animation.fps);
|
||||||
|
this.animation.interval = setInterval(() => this.handleAnimation(), ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
isWall(x, y) {
|
||||||
|
let mapX = x | 0;
|
||||||
|
let mapY = y | 0;
|
||||||
|
|
||||||
|
if (mapX < 0 || mapX >= this.map.width || mapY < 0 || mapY >= this.map.height) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.map.cells[mapY][mapX] !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
castRay(camX, camY, camAngle, angleOffset) {
|
||||||
|
const rayAngle = camAngle + angleOffset;
|
||||||
|
const rayX = Math.cos(rayAngle);
|
||||||
|
const rayY = Math.sin(rayAngle);
|
||||||
|
const fishEye = Math.cos(angleOffset); // corrects fish-eye effect https://stackoverflow.com/questions/66591163/how-do-i-fix-the-warped-perspective-in-my-raycaster
|
||||||
|
// const fishEye = 1;
|
||||||
|
|
||||||
|
let distance = Math.SQRT1_2 / 2;
|
||||||
|
let step = 0.0001;
|
||||||
|
|
||||||
|
while (distance < this.rendering.maxDepth) {
|
||||||
|
const testX = camX + rayX * distance;
|
||||||
|
const testY = camY + rayY * distance;
|
||||||
|
|
||||||
|
if (this.isWall(testX, testY)) {
|
||||||
|
return [
|
||||||
|
distance * fishEye,
|
||||||
|
{
|
||||||
|
// testX,
|
||||||
|
// testY,
|
||||||
|
// rayDistance: distance, // the distance the ray traveled, not the distance the object was away from us
|
||||||
|
color: (1 - distance / this.rendering.maxDepth) * 1.0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
distance += step;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [this.rendering.maxDepth, "#000"];
|
||||||
|
}
|
||||||
|
|
||||||
|
render(x, y, direction) {
|
||||||
|
if (!this.rendering.enabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rendering.ticker++;
|
||||||
|
|
||||||
|
x += 0.5;
|
||||||
|
y += 0.5;
|
||||||
|
|
||||||
|
const h = this.rendering.view.height;
|
||||||
|
const w = this.rendering.view.width;
|
||||||
|
|
||||||
|
// const middle = this.rendering.height / 2;
|
||||||
|
// Hack to simulate bouncy walking by moving the middle of the screen up and down a bit
|
||||||
|
const bounce = Math.sin(this.rendering.ticker / 4) * 0.2;
|
||||||
|
const middle = h / 2 + bounce;
|
||||||
|
|
||||||
|
for (let screenX = 0; screenX < w; screenX++) {
|
||||||
|
//
|
||||||
|
//
|
||||||
|
const rayOffset = (screenX / w) * this.rendering.fov - this.rendering.fov / 2;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Cast the ray, one ray per column, just like wolfenstein
|
||||||
|
const [distance, wall] = this.castRay(x, y, direction, rayOffset);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Start drawing
|
||||||
|
for (let screenY = 0; screenY < h; screenY++) {
|
||||||
|
//
|
||||||
|
// Calculate how high walls are at the distance of the ray's intersection
|
||||||
|
const wallH = middle / distance;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Given the current y-coordinate and distance, are we hitting the ceiling?
|
||||||
|
if (screenY < middle - wallH) {
|
||||||
|
this.rendering.view.put(screenX, screenY, "´", "#999");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Given the current y-coordinate and distance, are we hitting the floor?
|
||||||
|
if (screenY > middle + wallH) {
|
||||||
|
this.rendering.view.put(screenX, screenY, "~", "#b52");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// We've either hit a wall or the limit of our visibility,
|
||||||
|
// So we Determine the color of the pixel to draw.
|
||||||
|
const color = wall && wall.color ? wall.color : 1 - distance / this.rendering.maxDepth;
|
||||||
|
|
||||||
|
// TODO: Lerp these characters.
|
||||||
|
// const distancePalette = ["█", "▓", "▒", "░", " "];
|
||||||
|
const distancePalette = ["#", "%", "+", "÷", " "];
|
||||||
|
const char = distancePalette[distance | 0];
|
||||||
|
|
||||||
|
this.rendering.view.put(screenX, screenY, char, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.rendering.view.commitToDOM();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCompass() {
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Update the compass
|
||||||
|
document.getElementById("compass").textContent = sprintf(
|
||||||
|
"%s %s (%d --> %.2f [%dº])",
|
||||||
|
this.player._posV,
|
||||||
|
Object.keys(CardinalDirection)[this.player.orientation].toLowerCase(),
|
||||||
|
this.player.orientation,
|
||||||
|
this.player.orientation * PI_OVER_TWO,
|
||||||
|
this.player.orientation * 90,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMinimap() {
|
||||||
|
if (!this.player.withinAABB(this.map.width - 1, this.map.height - 1)) {
|
||||||
|
console.error("Player out of bounds");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Remove the old player symbol
|
||||||
|
const playerEl = document.querySelector(".player");
|
||||||
|
if (playerEl) {
|
||||||
|
playerEl.className = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is just for debugging!
|
||||||
|
//
|
||||||
|
|
||||||
|
for (let y = 0; y < this.map.height; y++) {
|
||||||
|
for (let x = 0; x < this.map.width; x++) {
|
||||||
|
this.minimap.elements[y][x].textContent = this.map.cells[y][x] ? "#" : " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the player token to the minimap
|
||||||
|
const dirForCSS = Object.keys(CardinalDirection)[this.player.orientation].toLowerCase();
|
||||||
|
this.minimap.elements[this.player.y][this.player.x].classList.add("player", dirForCSS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the game
|
||||||
|
window.game = new DungeonCrawler();
|
||||||
89
frontend/ascii_tile_map.js
Executable file
89
frontend/ascii_tile_map.js
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
export const defaultLegend = {
|
||||||
|
" ": {
|
||||||
|
type: "empty",
|
||||||
|
},
|
||||||
|
// Default Wall
|
||||||
|
"#": {
|
||||||
|
minimap: "#",
|
||||||
|
occupied: true,
|
||||||
|
type: "wall",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Column
|
||||||
|
"◯": {
|
||||||
|
minimap: "◯",
|
||||||
|
occupied: true,
|
||||||
|
type: "statue",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Closed Door
|
||||||
|
"░": {
|
||||||
|
minimap: "░",
|
||||||
|
occupied: true,
|
||||||
|
type: "sprite?????",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Where the player starts
|
||||||
|
"@": {
|
||||||
|
type: "playerStart",
|
||||||
|
},
|
||||||
|
|
||||||
|
// TRAP!
|
||||||
|
"☠": {
|
||||||
|
type: "trap",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Monster
|
||||||
|
"!": {
|
||||||
|
type: "monster",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export class Tile {
|
||||||
|
/** @param {object} options */
|
||||||
|
constructor(options) {
|
||||||
|
for (let [k, v] of Object.entries(options)) {
|
||||||
|
this[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class TileMap {
|
||||||
|
/** @param {string} str */
|
||||||
|
static fromText(str) {
|
||||||
|
const lines = str.split("\n");
|
||||||
|
|
||||||
|
const longestLine = lines.reduce((acc, line) => Math.max(acc, line.length), 0);
|
||||||
|
|
||||||
|
const tiles = new Array(lines.length).fill().map(() => new Array(longestLine));
|
||||||
|
|
||||||
|
lines.forEach((line, y) => {
|
||||||
|
line = line.padEnd(longestLine, "#");
|
||||||
|
|
||||||
|
line.split("").forEach((char, x) => {
|
||||||
|
const options = defaultLegend[char] ?? defaultLegend[" "];
|
||||||
|
tiles[y][x] = new Tile(options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new TileMap(tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Tile[]} tiles
|
||||||
|
*/
|
||||||
|
constructor(tiles) {
|
||||||
|
this.height = tiles.length;
|
||||||
|
this.width = tiles[0].length;
|
||||||
|
this.tiles = tiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = `
|
||||||
|
kim
|
||||||
|
har en
|
||||||
|
# meje #
|
||||||
|
stor pikkemand
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tm = TileMap.fromText(str);
|
||||||
|
|
||||||
|
console.log(tm.tiles);
|
||||||
196
frontend/ascrii_window.js
Executable file
196
frontend/ascrii_window.js
Executable file
@@ -0,0 +1,196 @@
|
|||||||
|
const grayscale = [
|
||||||
|
"#000",
|
||||||
|
"#111",
|
||||||
|
"#222",
|
||||||
|
"#333",
|
||||||
|
"#444",
|
||||||
|
"#555",
|
||||||
|
"#666",
|
||||||
|
"#777",
|
||||||
|
"#888",
|
||||||
|
"#999",
|
||||||
|
"#aaa",
|
||||||
|
"#bbb",
|
||||||
|
"#ccc",
|
||||||
|
"#ddd",
|
||||||
|
"#eee",
|
||||||
|
"#fff",
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeColor(color) {
|
||||||
|
if (typeof color === "number" && color >= 0 && color <= 1) {
|
||||||
|
return grayscale[Math.round(color * 16)];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof color === "number" && color >= 0 && color <= 16) {
|
||||||
|
return grayscale[color];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof color === "string" && color.length === 4 && color[0] === "#") {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Color could not be normalized");
|
||||||
|
}
|
||||||
|
|
||||||
|
class Patch {
|
||||||
|
/** @type {string} char */
|
||||||
|
char;
|
||||||
|
|
||||||
|
/** @type {number|string} fg foreground color */
|
||||||
|
bg;
|
||||||
|
|
||||||
|
/** @type {number|string} bg background color */
|
||||||
|
fg;
|
||||||
|
|
||||||
|
/** @param {HTMLElement} el */
|
||||||
|
constructor(el) {
|
||||||
|
/** @type {HTMLElement} el */
|
||||||
|
this.el = el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Pixel {
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} el
|
||||||
|
* @param {string} char
|
||||||
|
* @param {number|string} fg foreground color
|
||||||
|
* @param {number|string} bg background color
|
||||||
|
*/
|
||||||
|
constructor(el, char = " ", fg = "#fff", bg = undefined) {
|
||||||
|
//
|
||||||
|
/** @type {HTMLElement} el the html element that makes up this cell*/
|
||||||
|
this.el = el;
|
||||||
|
|
||||||
|
/** @type {string} char */
|
||||||
|
this.char = char;
|
||||||
|
|
||||||
|
/** @type {number|string} bg background color */
|
||||||
|
this.fg = fg === undefined ? undefined : normalizeColor(fg);
|
||||||
|
|
||||||
|
/** @type {number|string} fg foreground color */
|
||||||
|
this.bg = bg === undefined ? undefined : normalizeColor(bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new Pixel(this.el, this.car, this.fg, this.bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AsciiWindow {
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} container
|
||||||
|
* @param {number} width Canvas width (in pseudo-pixels)
|
||||||
|
* @param {number} height Canvas height (in pseudo-pixels)
|
||||||
|
*/
|
||||||
|
constructor(container, width, height) {
|
||||||
|
//
|
||||||
|
/** @type {HTMLElement} Paren element that contains all the pseudo-pixels */
|
||||||
|
this.container = container;
|
||||||
|
|
||||||
|
/** @type {number} width Canvas width (in pseudo-pixels) */
|
||||||
|
this.width = width;
|
||||||
|
|
||||||
|
/** @type {number} height Canvas height (in pseudo-pixels) */
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
/** @type {Pixel[][]} */
|
||||||
|
this.canvas = undefined;
|
||||||
|
|
||||||
|
/** @type {Patch[]} */
|
||||||
|
this.diff = [];
|
||||||
|
|
||||||
|
this.initializeCanvaas();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the html elements that make up the canvas,
|
||||||
|
* as well as a buffer that holds a copy of the data
|
||||||
|
* in the cells so we can diff properly.
|
||||||
|
*/
|
||||||
|
initializeCanvaas() {
|
||||||
|
const w = this.width;
|
||||||
|
const h = this.height;
|
||||||
|
|
||||||
|
/** @type {Pixel[][]} */
|
||||||
|
this.canvas = new Array(w).fill().map(() => new Array(h).fill().map(() => new Pixel()));
|
||||||
|
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const rowEl = document.createElement("div");
|
||||||
|
this.container.appendChild(rowEl);
|
||||||
|
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const pixelEl = document.createElement("code");
|
||||||
|
rowEl.appendChild(pixelEl);
|
||||||
|
pixelEl.textContent = " ";
|
||||||
|
this.canvas[y][x] = new Pixel(pixelEl, " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withinBounds(x, y) {
|
||||||
|
return x >= 0 && x < this.width && y >= 0 && y < this.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
mustBeWithinBounds(x, y) {
|
||||||
|
if (!this.withinBounds(x, y)) {
|
||||||
|
throw new Error(`Coordinate [${x}, ${y}] is out of bounds`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
put(x, y, char, fg = undefined, bg = undefined) {
|
||||||
|
//
|
||||||
|
this.mustBeWithinBounds(x, y);
|
||||||
|
const pixel = this.canvas[y][x];
|
||||||
|
|
||||||
|
const patch = new Patch(pixel.el);
|
||||||
|
|
||||||
|
fg = fg === undefined ? undefined : normalizeColor(fg);
|
||||||
|
bg = bg === undefined ? undefined : normalizeColor(bg);
|
||||||
|
|
||||||
|
let changeCount = 0;
|
||||||
|
|
||||||
|
// Check for changes in text contents
|
||||||
|
if (char !== undefined && char !== pixel.char) {
|
||||||
|
changeCount++;
|
||||||
|
patch.char = char;
|
||||||
|
pixel.char = char;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for changes in foreground color
|
||||||
|
if (fg !== undefined && fg !== pixel.fg) {
|
||||||
|
changeCount++;
|
||||||
|
patch.fg = fg;
|
||||||
|
pixel.fg = fg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for changes in background color
|
||||||
|
if (bg !== undefined && bg !== pixel.bg) {
|
||||||
|
changeCount++;
|
||||||
|
patch.bg = bg;
|
||||||
|
pixel.bg = bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeCount > 0) {
|
||||||
|
this.diff.push(patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply all patches to the DOM
|
||||||
|
*/
|
||||||
|
commitToDOM() {
|
||||||
|
this.diff.forEach((/** @type {Patch} */ patch) => {
|
||||||
|
if (patch.char !== undefined) {
|
||||||
|
patch.el.textContent = patch.char;
|
||||||
|
}
|
||||||
|
if (patch.fg !== undefined) {
|
||||||
|
patch.el.style.color = patch.fg;
|
||||||
|
}
|
||||||
|
if (patch.bg !== undefined) {
|
||||||
|
patch.el.style.backgroundColor = patch.bg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.diff = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
164
frontend/vec2.js
Executable file
164
frontend/vec2.js
Executable file
@@ -0,0 +1,164 @@
|
|||||||
|
export class Vector2i {
|
||||||
|
constructor(x = 0, y = 0) {
|
||||||
|
this.x = x | 0; // force int
|
||||||
|
this.y = y | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
clone() {
|
||||||
|
return new Vector2i(this.x, this.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add another vector (mutates)
|
||||||
|
add(v) {
|
||||||
|
this.x = (this.x + v.x) | 0;
|
||||||
|
this.y = (this.y + v.y) | 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// subtract another vector (mutates)
|
||||||
|
sub(v) {
|
||||||
|
this.x = (this.x - v.x) | 0;
|
||||||
|
this.y = (this.y - v.y) | 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiply by scalar (mutates)
|
||||||
|
mul(s) {
|
||||||
|
this.x = (this.x * s) | 0;
|
||||||
|
this.y = (this.y * s) | 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// length squared (int-safe)
|
||||||
|
lengthSq() {
|
||||||
|
return (this.x * this.x + this.y * this.y) | 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// temporary result (non-mutating)
|
||||||
|
added(v) {
|
||||||
|
return this.clone().add(v);
|
||||||
|
}
|
||||||
|
subbed(v) {
|
||||||
|
return this.clone().sub(v);
|
||||||
|
}
|
||||||
|
mulled(s) {
|
||||||
|
return this.clone().mul(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// which cardinal direction are we in?
|
||||||
|
cardinalDirection() {
|
||||||
|
if (this.y === 0) {
|
||||||
|
if (this.x > 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.x < 0) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.x === 0) {
|
||||||
|
if (this.y > 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.y < 0) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateCW(quarterTurns = 1) {
|
||||||
|
quarterTurns = (quarterTurns + 4) % 4;
|
||||||
|
if (quarterTurns === 0) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (quarterTurns === 1) {
|
||||||
|
return this.rotate270();
|
||||||
|
}
|
||||||
|
if (quarterTurns === 2) {
|
||||||
|
return this.rotate180();
|
||||||
|
}
|
||||||
|
if (quarterTurns === 3) {
|
||||||
|
return this.rotate90();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Logic error. How did we end up here?");
|
||||||
|
}
|
||||||
|
rotateCCW(quarterTurns = 1) {
|
||||||
|
quarterTurns = (quarterTurns + 4) % 4;
|
||||||
|
if (quarterTurns === 0) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quarterTurns === 1) {
|
||||||
|
return this.rotate90();
|
||||||
|
}
|
||||||
|
if (quarterTurns === 2) {
|
||||||
|
return this.rotate180();
|
||||||
|
}
|
||||||
|
if (quarterTurns === 3) {
|
||||||
|
return this.rotate270();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Logic error. How did we end up here?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === rotations (mutating) ===
|
||||||
|
rotate90() {
|
||||||
|
// counter-clockwise
|
||||||
|
const x = this.x | 0;
|
||||||
|
this.x = -this.y | 0;
|
||||||
|
this.y = x | 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate180() {
|
||||||
|
this.x = -this.x | 0;
|
||||||
|
this.y = -this.y | 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
rotate270() {
|
||||||
|
// clockwise
|
||||||
|
const x = this.x | 0;
|
||||||
|
this.x = this.y | 0;
|
||||||
|
this.y = -x | 0;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === non-mutating versions ===
|
||||||
|
rotated90() {
|
||||||
|
return this.clone().rotate90();
|
||||||
|
}
|
||||||
|
rotated180() {
|
||||||
|
return this.clone().rotate180();
|
||||||
|
}
|
||||||
|
rotated270() {
|
||||||
|
return this.clone().rotate270();
|
||||||
|
}
|
||||||
|
rotatedCW(quarterTurns) {
|
||||||
|
return this.clone().rotateCW(quarterTurns);
|
||||||
|
}
|
||||||
|
rotatedCCW(quarterTurns) {
|
||||||
|
return this.clone().rotateCCW(quarterTurns);
|
||||||
|
}
|
||||||
|
|
||||||
|
angle() {
|
||||||
|
return Math.atan2(this.y, this.x); // radians
|
||||||
|
}
|
||||||
|
|
||||||
|
angleTo(v) {
|
||||||
|
const a = this.angle();
|
||||||
|
const b = v.angle();
|
||||||
|
|
||||||
|
let d = (b - a) % (2 * Math.PI);
|
||||||
|
if (d < -Math.PI) d += 2 * Math.PI;
|
||||||
|
if (d > Math.PI) d -= 2 * Math.PI;
|
||||||
|
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `[${this.x} , ${this.y}]`;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user