-
-
Save mamamia5x/b4db683c176604ad923dec5e259ca45b to your computer and use it in GitHub Desktop.
SCRIPT-8
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// title: Ray Caster | |
class V { | |
_argumentsToXY(args) { | |
return args.length === 1 ? args[0] : { x: args[0], y: args[1] } | |
} | |
constructor() { | |
const { x, y } = this._argumentsToXY(arguments) | |
this.x = x | |
this.y = y | |
} | |
add() { | |
const { x, y } = this._argumentsToXY(arguments) | |
return new V(this.x + x, this.y + y) | |
} | |
sub() { | |
const { x, y } = this._argumentsToXY(arguments) | |
return new V(this.x - x, this.y - y) | |
} | |
multiply() { | |
if (arguments.length === 1 && typeof arguments[0] === 'number') { | |
return new V(this.x * arguments[0], this.y * arguments[0]) | |
} | |
const { x, y } = this._argumentsToXY(arguments) | |
return new V(this.x * x, this.y * y) | |
} | |
floor() { | |
return new V(Math.floor(this.x), Math.floor(this.y)) | |
} | |
mod() { | |
return new V(this.x % 1, this.y % 1) | |
} | |
rotate(angle) { | |
const cos = Math.cos(angle) | |
const sin = Math.sin(angle) | |
return new V(this.x * cos - this.y * sin, this.x * sin + this.y * cos) | |
} | |
get half() { | |
return new V(this.x / 2, this.y / 2) | |
} | |
} | |
class Rect { | |
_argumentsToPosSize(args) { | |
return args.length === 1 | |
? { pos: new V(args[0].pos), size: new V(args[0].size) } | |
: args.length === 2 | |
? { pos: new V(args[0]), size: new V(args[1]) } | |
: { | |
pos: new V(args[0], args[1]), | |
size: new V(args[2], args[3]) | |
} | |
} | |
constructor() { | |
const { pos, size } = this._argumentsToPosSize(arguments) | |
this.pos = pos | |
this.size = size | |
} | |
grow(sizeDelta) { | |
return new Rect(this.pos.sub(sizeDelta.half), this.size.add(sizeDelta)) | |
} | |
get center() { | |
return new V(this.pos.x + this.size.x / 2, this.pos.y + this.size.y / 2) | |
} | |
} | |
const getRayVsRectCollisionPoint = (pos, velocity, rect) => { | |
const A = rect.pos | |
const B = rect.pos.add(rect.size) | |
let near, far, remainingVelocity | |
if (velocity.y === 0) { | |
if (velocity.x === 0) return null | |
if (pos.y <= A.y || pos.y >= B.y) return null | |
let [tx1, tx2] = [ | |
(A.x - pos.x) / velocity.x, | |
(B.x - pos.x) / velocity.x | |
].sort((a, b) => a - b) | |
near = tx1 | |
far = tx2 | |
if (far <= 0) return null | |
if (near < 0 || near >= 1) return null | |
remainingVelocity = new V(0, 0) | |
} else if (velocity.x === 0) { | |
if (velocity.y === 0) return null | |
if (pos.x <= A.x || pos.x >= B.x) return null | |
let [ty1, ty2] = [ | |
(A.y - pos.y) / velocity.y, | |
(B.y - pos.y) / velocity.y | |
].sort((a, b) => a - b) | |
near = ty1 | |
far = ty2 | |
if (far <= 0) return null | |
if (near < 0 || near >= 1) return null | |
remainingVelocity = new V(0, 0) | |
} else { | |
let [tx1, tx2] = [ | |
(A.x - pos.x) / velocity.x, | |
(B.x - pos.x) / velocity.x | |
].sort((a, b) => a - b) | |
let [ty1, ty2] = [ | |
(A.y - pos.y) / velocity.y, | |
(B.y - pos.y) / velocity.y | |
].sort((a, b) => a - b) | |
if (tx1 > ty2 || ty1 > tx2) return null | |
far = tx2 > ty2 ? ty2 : tx2 | |
near = tx1 > ty1 ? tx1 : ty1 | |
if (far <= 0) return null | |
if (near < 0 || near >= 1) return null | |
remainingVelocity = velocity | |
.multiply(tx1 > ty1 ? new V(0, 1) : new V(1, 0)) | |
.multiply(new V(1 - near, 1 - near)) | |
} | |
return { | |
near, | |
point: pos.add(velocity.multiply(near)), | |
remainingVelocity | |
} | |
} | |
const getDynamicRectVsRectCollisionPoint = ( | |
dynamicRect, | |
velocityVector, | |
rect | |
) => { | |
return getRayVsRectCollisionPoint( | |
dynamicRect.center, | |
velocityVector, | |
rect.grow(dynamicRect.size) | |
) | |
} | |
const resolveCollisions = (player, velocityVector, obstacles) => { | |
const halfPlayerSize = player.size.half | |
let nextPlayer = new Rect(player) | |
let nextVelocityVector = new V(velocityVector) | |
for (let j = 0; j < 2; j++) { | |
const collisions = obstacles | |
.map(obstacle => | |
getDynamicRectVsRectCollisionPoint( | |
nextPlayer, | |
nextVelocityVector, | |
new Rect(obstacle) | |
) | |
) | |
.filter(a => a) | |
.sort((a, b) => a.near - b.near) | |
const collision = collisions[0] | |
if (!collision) break | |
nextPlayer.pos = collision.point.sub(halfPlayerSize) | |
nextVelocityVector = collision.remainingVelocity | |
} | |
nextPlayer.pos = nextPlayer.pos.add(nextVelocityVector) | |
return nextPlayer | |
} | |
const getCell = (v, array) => { | |
if (v.x < 0 || v.y < 0) return -1 | |
const row = array[Math.floor(v.y)] | |
if (row === undefined) return -1 | |
const cell = row[Math.floor(v.x)] | |
if (cell === undefined) return -1 | |
return cell | |
} | |
const calculateVisionRay = (x, y, cx, cy, dx, dy, dir, iMax, level) => { | |
let length = 0 | |
let tX = 0 | |
if (dir.x === 1 && dir.y === 0) { | |
let iX = cx | |
for (let i = 1; i < iMax; i++) { | |
if (getCell(new V(iX + 0.5, cy), level)) { | |
break | |
} | |
iX += 1 | |
} | |
length = iX - x | |
} else if (dir.x === -1 && dir.y === 0) { | |
let iX = cx | |
for (let i = 1; i < iMax; i++) { | |
if (getCell(new V(iX - 0.5, cy), level)) { | |
break | |
} | |
iX -= 1 | |
} | |
length = x - iX | |
} else if (dir.x === 0 && dir.y === 1) { | |
let iY = cy | |
for (let i = 1; i < iMax; i++) { | |
if (getCell(new V(cx, iY + 0.5), level)) { | |
break | |
} | |
iY += 1 | |
} | |
length = iY - y | |
} else if (dir.x === 0 && dir.y === -1) { | |
let iY = cy | |
for (let i = 1; i < iMax; i++) { | |
if (getCell(new V(cx, iY - 0.5), level)) { | |
break | |
} | |
iY -= 1 | |
} | |
length = y - iY | |
} else { | |
const tan = dir.y / dir.x | |
const tileStepX = dir.x > 0 ? 1 : dir.x < 0 ? -1 : 0 | |
const tileStepY = dir.y > 0 ? 1 : dir.y < 0 ? -1 : 0 | |
let xIntercept = new V( | |
cx + (tileStepX > 0 ? 1 : 0), | |
y + (tileStepX > 0 ? 1 - dx : dx) * tan * tileStepX | |
) | |
let yIntercept = new V( | |
x + ((tileStepY > 0 ? 1 - dy : dy) / tan) * tileStepY, | |
cy + (tileStepY > 0 ? 1 : 0) | |
) | |
for (let i = 1; i < iMax; i++) { | |
if (getCell(xIntercept.add(tileStepX * 0.5, 0), level)) { | |
break | |
} | |
xIntercept = xIntercept.add(tileStepX, tan * tileStepX) | |
} | |
for (let i = 1; i < iMax; i++) { | |
if (getCell(yIntercept.add(0, tileStepY * 0.5), level)) { | |
break | |
} | |
yIntercept = yIntercept.add(tileStepY / tan, tileStepY) | |
} | |
const xILength = Math.abs((xIntercept.x - x) / dir.x) | |
const yILength = Math.abs((yIntercept.x - x) / dir.x) | |
if (xILength < yILength) { | |
length = xILength | |
tX = xIntercept.y % 1 | |
} else { | |
length = yILength | |
tX = yIntercept.x % 1 | |
} | |
} | |
return { length, tX } | |
} | |
const calculateVisionRays = ( | |
x, | |
y, | |
cx, | |
cy, | |
dx, | |
dy, | |
dir, | |
iMax, | |
level, | |
screen | |
) => { | |
const fov = Math.PI / 3 | |
const fppWidth = screen.size.x | |
const fppHeight = screen.size.y | |
const fppHalfHeight = screen.center.y | |
const steps = fppWidth | |
const rayStep = fov / (steps - 1) | |
const rayDirOffset = fov * 0.5 | |
const rayDirStart = dir.rotate(-rayDirOffset) | |
const b = fppHeight * 0.75 | |
const lines = [] | |
for (let i = 0; i < steps; i++) { | |
const rayDir = rayDirStart.rotate(rayStep * i) | |
const { length, tX } = calculateVisionRay( | |
x, | |
y, | |
cx, | |
cy, | |
dx, | |
dy, | |
rayDir, | |
iMax, | |
level | |
) | |
const adjustedLength = new V(length, 0).rotate(rayStep * i - rayDirOffset).x | |
const lineHeight = b / adjustedLength | |
const halfLineHeight = lineHeight / 2 | |
lines.push([ | |
i, | |
fppHalfHeight - halfLineHeight, | |
fppHalfHeight + halfLineHeight, | |
4 + (clamp(tX, 0.025, 0.975) === tX ? 0 : 1) + (3 * length) / (iMax - 1) | |
]) | |
} | |
return lines | |
} | |
function createPreCalculatedThresholdMatrix(n) { | |
function r(array) { | |
const size = array.length * 2 | |
const matrix = range(size).map(() => range(size)) | |
for (let y = 0; y < size; y++) | |
for (let x = 0; x < size; x++) { | |
let value, | |
base = 4 * array[y % array.length][x % array.length] | |
if (y < array.length && x < array.length) value = base | |
else if (x < array.length) value = base + 3 | |
else if (y < array.length) value = base + 2 | |
else value = base + 1 | |
matrix[y][x] = value | |
} | |
return matrix | |
} | |
let array = [] | |
for (let m = 1; m <= n; m++) { | |
if (m === 1) { | |
array = [ | |
[0, 2], | |
[3, 1] | |
] | |
} else { | |
array = r(array) | |
} | |
} | |
for (let y = 0; y < array.length; y++) | |
for (let x = 0; x < array.length; x++) | |
array[y][x] = (array[y][x] + 1) / (array.length * array.length) - 0.5 | |
return array | |
} | |
function createOrderedDitheringFunc(n) { | |
const thresholdMatrix = createPreCalculatedThresholdMatrix(n) | |
const size = thresholdMatrix.length | |
return (x, y, buffer) => { | |
const factor = thresholdMatrix[y % size][x % size] | |
const attempt = buffer[x][y] + factor | |
buffer[x][y] = clamp(Math.round(attempt), 0, 7) | |
} | |
} | |
const dither = createOrderedDitheringFunc(2) | |
function bufferLine(x, y1, y2, color, buffer) { | |
y1 = Math.round(y1) | |
y2 = Math.round(y2) | |
for (let i = y1; i < y2; i++) { | |
buffer[x][i] = color | |
} | |
} | |
const screenSize = 128 | |
const mapSize = 32 | |
const level = [ | |
[1, 1, 1, 1, 1, 1, 1, 1], | |
[1, 0, 0, 0, 0, 0, 0, 1], | |
[1, 0, 0, 0, 1, 1, 0, 1], | |
[1, 0, 0, 0, 0, 0, 0, 1], | |
[1, 0, 0, 0, 0, 1, 0, 1], | |
[1, 0, 0, 0, 1, 1, 0, 1], | |
[1, 0, 0, 0, 0, 0, 0, 1], | |
[1, 1, 1, 1, 1, 1, 1, 1] | |
] | |
const moveSpeedInSquares = 2 // per second | |
const rotationSpeedInDegrees = 120 // per second | |
const moveSpeed = moveSpeedInSquares / 1000 // squares per millisecond | |
const rotationSpeed = (rotationSpeedInDegrees * Math.PI) / 180 / 1000 // radians per millisecond | |
const mapCellSize = mapSize / level.length | |
const playerSize = 0.5 | |
const iMax = 8 | |
init = state => { | |
state.playerPos = [1.5 - playerSize / 2, 6.5 - playerSize / 2] | |
state.playerCell = [ | |
Math.floor(state.playerPos[0]), | |
Math.floor(state.playerPos[1]) | |
] | |
state.playerDir = [1.0, 0.0] | |
state.debug = {} | |
} | |
update = (state, input, elapsed) => { | |
const obstacles = [] | |
let velocity = new V(0, 0) | |
let nextPlayerDir = state.playerDir | |
if (input.up) { | |
const delta = elapsed * moveSpeed | |
velocity = new V(state.playerDir[0] * delta, state.playerDir[1] * delta) | |
} | |
if (input.down) { | |
const delta = elapsed * moveSpeed | |
velocity = new V(state.playerDir[0] * -delta, state.playerDir[1] * -delta) | |
} | |
if (input.right) { | |
const delta = elapsed * rotationSpeed | |
nextPlayerDir = [ | |
state.playerDir[0] * Math.cos(delta) - | |
state.playerDir[1] * Math.sin(delta), | |
state.playerDir[0] * Math.sin(delta) + | |
state.playerDir[1] * Math.cos(delta) | |
] | |
} | |
if (input.left) { | |
const delta = elapsed * rotationSpeed | |
nextPlayerDir = [ | |
state.playerDir[0] * Math.cos(-delta) - | |
state.playerDir[1] * Math.sin(-delta), | |
state.playerDir[0] * Math.sin(-delta) + | |
state.playerDir[1] * Math.cos(-delta) | |
] | |
} | |
const { x: x1, y: y1 } = new V(state.playerPos[0], state.playerPos[1]) | |
const { x: x2, y: y2 } = new V(state.playerPos[0], state.playerPos[1]).add( | |
velocity | |
) | |
const range = { | |
start: new V(x1 < x2 ? x1 : x2, y1 < y2 ? y1 : y2).floor(), | |
end: new V(x1 < x2 ? x2 : x1, y1 < y2 ? y2 : y1) | |
.add(playerSize, playerSize) | |
.floor() | |
} | |
for (let y = range.start.y; y <= range.end.y; y++) | |
for (let x = range.start.x; x <= range.end.x; x++) | |
level[y][x] && obstacles.push(new Rect(x, y, 1, 1)) | |
const player = resolveCollisions( | |
new Rect(...state.playerPos, playerSize, playerSize), | |
velocity, | |
obstacles.filter(a => a) | |
) | |
const { x, y } = player.center | |
const { x: cx, y: cy } = player.center.floor() | |
const { x: dx, y: dy } = player.center.mod() | |
const lines = calculateVisionRays( | |
x, | |
y, | |
cx, | |
cy, | |
dx, | |
dy, | |
new V(nextPlayerDir[0], nextPlayerDir[1]), | |
iMax, | |
level, | |
new Rect(0, 0, screenSize, screenSize) | |
) | |
state.lines = lines | |
state.playerCell = [Math.floor(player.pos.x), Math.floor(player.pos.y)] | |
state.playerPos = [player.pos.x, player.pos.y] | |
state.playerDir = nextPlayerDir | |
} | |
draw = state => { | |
clear() | |
// main | |
const buffer = range(128).map(() => | |
range(128).map((a, i) => (i < 128 / 2 ? 7 : 7.5 - i / 128)) | |
) | |
state.lines.forEach(args => { | |
const [x, y1, y2, color] = args | |
bufferLine(x, y1, y2, color, buffer) | |
}) | |
for (let y = 0; y < 128; y++) { | |
for (let x = 0; x < 128; x++) { | |
dither(x, y, buffer) | |
setPixel(x, y, buffer[x][y]) | |
} | |
} | |
// minimap | |
for (let y = 0; y < level.length; y++) | |
for (let x = 0; x < level[0].length; x++) | |
if (level[y][x] !== 0) | |
rectFill(x * mapCellSize, y * mapCellSize, mapCellSize, mapCellSize, 5) | |
else | |
rectFill(x * mapCellSize, y * mapCellSize, mapCellSize, mapCellSize, 6) | |
const scaledPlayerPos = new V( | |
state.playerPos[0] * mapCellSize, | |
state.playerPos[1] * mapCellSize | |
) | |
const scaledPlayerSize = playerSize * mapCellSize | |
const scaledPlayerHalfSize = scaledPlayerSize / 2 | |
rectFill( | |
scaledPlayerPos.x, | |
scaledPlayerPos.y, | |
scaledPlayerSize, | |
scaledPlayerSize, | |
4 | |
) | |
line( | |
scaledPlayerPos.x + scaledPlayerHalfSize, | |
scaledPlayerPos.y + scaledPlayerHalfSize, | |
scaledPlayerPos.x + | |
scaledPlayerHalfSize + | |
state.playerDir[0] * 1 * mapCellSize, | |
scaledPlayerPos.y + | |
scaledPlayerHalfSize + | |
state.playerDir[1] * 1 * mapCellSize, | |
3 | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"iframeVersion": "0.1.280", | |
"lines": [ | |
77, | |
295, | |
158, | |
0, | |
0, | |
0, | |
0, | |
0 | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment