UPDATE

上级 649065bb
"use strict";
const s = "\u6B22\u8FCE\u6765\u5230 InsCode";
console.log(s);
//# sourceMappingURL=index.js.map
{
"version": 3,
"sources": ["../index.ts"],
"sourcesContent": ["const s: string = \"\u6B22\u8FCE\u6765\u5230 InsCode\";\nconsole.log(s);\n"],
"mappings": ";AAAA,MAAM,IAAY;AAClB,QAAQ,IAAI,CAAC;",
"names": []
}
#!/usr/bin/env bash
set -e
rm -rf .build
mkdir -p ./.build
ENTRYPOINTS=$(find -type f -name '*.[tj]s' -not -path './node_modules/*')
esbuild $ENTRYPOINTS \
--log-level=warning \
--outdir='./.build' \
--outbase=. \
--sourcemap \
--target='node16' \
--platform='node' \
--format='cjs'
node_modules
.DS_Store
dist
dist-ssr
*.local
*-lock.yaml
run = "node --enable-source-maps .build/index.js"
run = "upm add guess;npm run dev"
entrypoint = "index.ts"
[nix]
......
{"version":2,"languages":{"nodejs-yarn":{"specfileHash":"abab35c9ba66256c189aa42c5e29aa02","lockfileHash":"b4fe953f33f9be5c0086b24be0e7aad5"}}}
MIT License
Copyright (c) 2022 Vyse12138
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# minecraft-threejs
English | [中文](https://github.com/vyse12138/minecraft-threejs/blob/main/README_ZH.md)
Minecraft clone made with Three.js and TypeScript
Check it out: [Minecraft - Three.js](https://mc.yulei.me/)
Recommend to use Chrome to get the best experience
![preview](https://user-images.githubusercontent.com/88306344/154383952-9b33bad4-eebb-4a98-a12e-f5f137422d06.gif)
## Features:
- Destroy block and place block
- Mouse wheel (or number key) to select different block types
- Movement and collision detection
- Random terrain / blocks / tree generations
- Infinite world
- Game save / load
- Sound effect and BGM
- Highlight the block at crosshair
- Basic UI and settings
- Mobile friendly
## Working on:
- Multiplayer
- Generates water
- Higher performance algorithm
# minecraft-threejs
中文 | [English](https://github.com/vyse12138/minecraft-threejs)
基于 Three.js 和 TypeScript 的网页版我的世界
试玩:[Minecraft - Three.js](https://mcz.yulei.me)
建议使用 Chrome 来获得最佳的体验
![preview](https://user-images.githubusercontent.com/88306344/154383952-9b33bad4-eebb-4a98-a12e-f5f137422d06.gif)
## 已支持:
- 方块的放置 / 破坏
- 鼠标滚轮(数字键)选择不同的方块类型
- 移动和碰撞检测
- 随机的地形和树木生成
- 无限的世界
- 保存 / 读取游戏
- 音效和背景音乐
- 可调节的渲染距离和视野范围
- 高亮准心方块
- 基本的 UI
- 支持桌面以及手机试玩
## 开发中:
- 更高效的算法(时间切片渲染算法和方块放置 / 破坏算法)
- 生成水
- 联机模式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/src/static/mc.ico" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1"
/>
<meta
name="description"
content="Minecraft clone made with Three.js + TypeScript, play minecraft in your own browser!"
/>
<title>Minecraft - Three.js</title>
</head>
<body>
<script type="module" src="./src/main.ts"></script>
<a
href="https://github.com/Vyse12138/minecraft-threejs"
title="Source on GitHub"
class="github"
target="_blank"
rel="noopener"
>
<svg width="72" height="72" viewBox="0 0 250 250">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path
d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="white"
></path>
<path
d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="white"
></path>
</svg>
</a>
<div class="menu start">
<button id="play" class="button">Play</button>
<button id="save" class="button">Load Game</button>
<button id="setting" class="button">Setting</button>
<button id="feature" class="button">Guide</button>
<button id="exit" class="button">Exit</button>
<footer class="footer">© 2022 Yulei Zhu</footer>
</div>
<div class="features hidden">
<p>
<b> Left-Click: </b> destroy block <br />
<b> Right-Click:</b> place block <br />
<b> Wheel / Number Key: </b>change block <br />
<b> WASD: </b>move <br />
<b> Space: </b>jump <br />
<b> Q:</b> normal / dev mode <br />
<b> Shift:</b> fly up (in dev mode) <br />
<b> F: </b>full screen <br />
<b> E: </b>menu <br />
</p>
<button id="back" class="button">Back</button>
</div>
<div class="settings hidden">
<p id="distance">Render Distance: 3</p>
<input
type="range"
id="distance-input"
min="1"
max="8"
value="3"
step="1"
/>
<p id="fov">Field of View: 70</p>
<input
type="range"
id="fov-input"
min="40"
max="100"
value="70"
step="1"
/>
<p id="music">Music: On</p>
<input type="range" id="music-input" min="0" max="1" value="1" step="1" />
<br />
<br />
<button id="setting-back" class="button">Apply</button>
</div>
<div class="save-modal hidden">Game Saved!</div>
<div class="load-modal hidden">Game Loaded!</div>
</body>
</html>
const s: string = "欢迎来到 InsCode";
console.log(s);
{
"name": "minecraft-threejs",
"version": "0.0.0",
"license": "MIT",
"author": "Yulei Zhu",
"description": "Minecraft clone made with Three.js and TypeScript",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 8080",
"lint": "tsc --noEmit"
},
"devDependencies": {
"@types/three": "^0.137.0",
"typescript": "^4.5.5",
"vite": "^2.8.0"
},
"dependencies": {
"guess": "^1.0.2",
"three": "^0.137.0"
}
}
import * as THREE from 'three'
import hal3 from './musics/hal3.ogg'
import { BlockType } from '../terrain'
import grass1 from './blocks/grass1.ogg'
import grass2 from './blocks/grass2.ogg'
import grass3 from './blocks/grass3.ogg'
import grass4 from './blocks/grass4.ogg'
import sand1 from './blocks/sand1.ogg'
import sand2 from './blocks/sand2.ogg'
import sand3 from './blocks/sand3.ogg'
import sand4 from './blocks/sand4.ogg'
import stone1 from './blocks/stone1.ogg'
import stone2 from './blocks/stone2.ogg'
import stone3 from './blocks/stone3.ogg'
import stone4 from './blocks/stone4.ogg'
import dirt1 from './blocks/dirt1.ogg'
import dirt2 from './blocks/dirt2.ogg'
import dirt3 from './blocks/dirt3.ogg'
import dirt4 from './blocks/dirt4.ogg'
import tree1 from './blocks/tree1.ogg'
import tree2 from './blocks/tree2.ogg'
import tree3 from './blocks/tree3.ogg'
import tree4 from './blocks/tree4.ogg'
import leaf1 from './blocks/leaf1.ogg'
import leaf2 from './blocks/leaf2.ogg'
import leaf3 from './blocks/leaf3.ogg'
import leaf4 from './blocks/leaf4.ogg'
import { isMobile } from '../utils'
export default class Audio {
constructor(camera: THREE.PerspectiveCamera) {
if (isMobile) return
const listener = new THREE.AudioListener()
const audioLoader = new THREE.AudioLoader()
camera.add(listener)
// load bgm
const bgm = new THREE.Audio(listener)
bgm.autoplay = false
audioLoader.load(hal3, buffer => {
bgm.setBuffer(buffer)
bgm.setVolume(0.1)
bgm.setLoop(true)
if (bgm.isPlaying) {
bgm.pause()
bgm.play()
}
})
// play / pause bgm
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement && !bgm.isPlaying && !this.disabled) {
bgm.play()
} else {
bgm.pause()
}
})
// load sound effect
for (const types of this.sourceSet) {
const audios: THREE.Audio[] = []
for (const type of types) {
audioLoader.load(type, buffer => {
const audio = new THREE.Audio(listener!)
audio.setBuffer(buffer)
audio.setVolume(0.15)
audios.push(audio)
})
}
this.soundSet.push(audios)
}
}
disabled = false
sourceSet = [
[grass1, grass2, grass3, grass4],
[sand1, sand2, sand3, sand4],
[tree1, tree2, tree3, tree4],
[leaf1, leaf2, leaf3, leaf4],
[dirt1, dirt2, dirt3, dirt4],
[stone1, stone2, stone3, stone4],
[stone1, stone2, stone3, stone4],
[tree1, tree2, tree3, tree4],
[stone1, stone2, stone3, stone4],
[stone1, stone2, stone3, stone4],
[stone1, stone2, stone3, stone4]
]
soundSet: THREE.Audio[][] = []
index = 0
playSound(type: BlockType) {
if (!this.disabled && !isMobile) {
this.index++ === 3 && (this.index = 0)
this.soundSet[type]?.[this.index]?.play()
}
}
}
import * as THREE from 'three'
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls'
import Player, { Mode } from '../player'
import Terrain, { BlockType } from '../terrain'
import Block from '../terrain/mesh/block'
import Noise from '../terrain/noise'
import Audio from '../audio'
import { isMobile } from '../utils'
enum Side {
front,
back,
left,
right,
down,
up
}
export default class Control {
constructor(
scene: THREE.Scene,
camera: THREE.PerspectiveCamera,
player: Player,
terrain: Terrain,
audio: Audio
) {
this.scene = scene
this.camera = camera
this.player = player
this.terrain = terrain
this.control = new PointerLockControls(camera, document.body)
this.audio = audio
this.raycaster = new THREE.Raycaster()
this.raycaster.far = 8
this.far = this.player.body.height
this.initRayCaster()
this.initEventListeners()
}
// core properties
scene: THREE.Scene
camera: THREE.PerspectiveCamera
player: Player
terrain: Terrain
control: PointerLockControls
audio: Audio
velocity = new THREE.Vector3(0, 0, 0)
// collide and jump properties
frontCollide = false
backCollide = false
leftCollide = false
rightCollide = false
downCollide = true
upCollide = false
isJumping = false
raycasterDown = new THREE.Raycaster()
raycasterUp = new THREE.Raycaster()
raycasterFront = new THREE.Raycaster()
raycasterBack = new THREE.Raycaster()
raycasterRight = new THREE.Raycaster()
raycasterLeft = new THREE.Raycaster()
tempMesh = new THREE.InstancedMesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial(),
100
)
tempMeshMatrix = new THREE.InstancedBufferAttribute(
new Float32Array(100 * 16),
16
)
// other properties
p1 = performance.now()
p2 = performance.now()
raycaster: THREE.Raycaster
far: number
holdingBlock = BlockType.grass
holdingBlocks = [
BlockType.grass,
BlockType.stone,
BlockType.tree,
BlockType.wood,
BlockType.diamond,
BlockType.quartz,
BlockType.glass,
BlockType.grass,
BlockType.grass,
BlockType.grass
]
holdingIndex = 0
wheelGap = false
clickInterval?: ReturnType<typeof setInterval>
jumpInterval?: ReturnType<typeof setInterval>
mouseHolding = false
spaceHolding = false
initRayCaster = () => {
this.raycasterUp.ray.direction = new THREE.Vector3(0, 1, 0)
this.raycasterDown.ray.direction = new THREE.Vector3(0, -1, 0)
this.raycasterFront.ray.direction = new THREE.Vector3(1, 0, 0)
this.raycasterBack.ray.direction = new THREE.Vector3(-1, 0, 0)
this.raycasterLeft.ray.direction = new THREE.Vector3(0, 0, -1)
this.raycasterRight.ray.direction = new THREE.Vector3(0, 0, 1)
this.raycasterUp.far = 1.2
this.raycasterDown.far = this.player.body.height
this.raycasterFront.far = this.player.body.width
this.raycasterBack.far = this.player.body.width
this.raycasterLeft.far = this.player.body.width
this.raycasterRight.far = this.player.body.width
}
setMovementHandler = (e: KeyboardEvent) => {
if (e.repeat) {
return
}
switch (e.key) {
case 'q':
if (this.player.mode === Mode.walking) {
this.player.setMode(Mode.flying)
} else {
this.player.setMode(Mode.walking)
}
this.velocity.y = 0
this.velocity.x = 0
this.velocity.z = 0
break
case 'w':
case 'W':
this.velocity.x += this.player.speed
break
case 's':
case 'S':
this.velocity.x -= this.player.speed
break
case 'a':
case 'A':
this.velocity.z -= this.player.speed
break
case 'd':
case 'D':
this.velocity.z += this.player.speed
break
case ' ':
if (this.player.mode === Mode.walking) {
// jump
if (!this.isJumping) {
this.velocity.y = 8
this.isJumping = true
this.downCollide = false
this.far = 0
setTimeout(() => {
this.far = this.player.body.height
}, 300)
}
} else {
this.velocity.y += this.player.speed
}
if (this.player.mode === Mode.walking && !this.spaceHolding) {
this.spaceHolding = true
this.jumpInterval = setInterval(() => {
this.setMovementHandler(e)
}, 10)
}
break
case 'Shift':
if (this.player.mode === Mode.walking) {
} else {
this.velocity.y -= this.player.speed
}
break
default:
break
}
}
resetMovementHandler = (e: KeyboardEvent) => {
if (e.repeat) {
return
}
switch (e.key) {
case 'w':
case 'W':
this.velocity.x = 0
break
case 's':
case 'S':
this.velocity.x = 0
break
case 'a':
case 'A':
this.velocity.z = 0
break
case 'd':
case 'D':
this.velocity.z = 0
break
case ' ':
this.jumpInterval && clearInterval(this.jumpInterval)
this.spaceHolding = false
if (this.player.mode === Mode.walking) {
return
}
this.velocity.y = 0
break
case 'Shift':
if (this.player.mode === Mode.walking) {
return
}
this.velocity.y = 0
break
default:
break
}
}
mousedownHandler = (e: MouseEvent) => {
e.preventDefault()
// let p1 = performance.now()
this.raycaster.setFromCamera({ x: 0, y: 0 }, this.camera)
const block = this.raycaster.intersectObjects(this.terrain.blocks)[0]
const matrix = new THREE.Matrix4()
switch (e.button) {
// left click to remove block
case 0:
{
if (block && block.object instanceof THREE.InstancedMesh) {
// calculate position
block.object.getMatrixAt(block.instanceId!, matrix)
const position = new THREE.Vector3().setFromMatrixPosition(matrix)
// don't remove bedrock
if (
(BlockType[block.object.name as any] as unknown as BlockType) ===
BlockType.bedrock
) {
this.terrain.generateAdjacentBlocks(position)
return
}
// remove the block
block.object.setMatrixAt(
block.instanceId!,
new THREE.Matrix4().set(
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
)
)
// block and sound effect
this.audio.playSound(
BlockType[block.object.name as any] as unknown as BlockType
)
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
this.terrain.materials.get(
this.terrain.materialType[
parseInt(BlockType[block.object.name as any])
]
)
)
mesh.position.set(position.x, position.y, position.z)
this.scene.add(mesh)
const time = performance.now()
let raf = 0
const animate = () => {
if (performance.now() - time > 250) {
this.scene.remove(mesh)
cancelAnimationFrame(raf)
return
}
raf = requestAnimationFrame(animate)
mesh.geometry.scale(0.85, 0.85, 0.85)
}
animate()
// update
block.object.instanceMatrix.needsUpdate = true
// check existence
let existed = false
for (const customBlock of this.terrain.customBlocks) {
if (
customBlock.x === position.x &&
customBlock.y === position.y &&
customBlock.z === position.z
) {
existed = true
customBlock.placed = false
}
}
// add to custom blocks when it's not existed
if (!existed) {
this.terrain.customBlocks.push(
new Block(
position.x,
position.y,
position.z,
BlockType[block.object.name as any] as unknown as BlockType,
false
)
)
}
// generate adjacent blocks
this.terrain.generateAdjacentBlocks(position)
}
}
break
// right click to put block
case 2:
{
if (block && block.object instanceof THREE.InstancedMesh) {
// calculate normal and position
const normal = block.face!.normal
block.object.getMatrixAt(block.instanceId!, matrix)
const position = new THREE.Vector3().setFromMatrixPosition(matrix)
// return when block overlaps with player
if (
position.x + normal.x === Math.round(this.camera.position.x) &&
position.z + normal.z === Math.round(this.camera.position.z) &&
(position.y + normal.y === Math.round(this.camera.position.y) ||
position.y + normal.y ===
Math.round(this.camera.position.y - 1))
) {
return
}
// put the block
matrix.setPosition(
normal.x + position.x,
normal.y + position.y,
normal.z + position.z
)
this.terrain.blocks[this.holdingBlock].setMatrixAt(
this.terrain.getCount(this.holdingBlock),
matrix
)
this.terrain.setCount(this.holdingBlock)
//sound effect
this.audio.playSound(this.holdingBlock)
// update
this.terrain.blocks[this.holdingBlock].instanceMatrix.needsUpdate =
true
// add to custom blocks
this.terrain.customBlocks.push(
new Block(
normal.x + position.x,
normal.y + position.y,
normal.z + position.z,
this.holdingBlock,
true
)
)
}
}
break
default:
break
}
if (!isMobile && !this.mouseHolding) {
this.mouseHolding = true
this.clickInterval = setInterval(() => {
this.mousedownHandler(e)
}, 333)
}
// console.log(performance.now() - p1)
}
mouseupHandler = () => {
this.clickInterval && clearInterval(this.clickInterval)
this.mouseHolding = false
}
changeHoldingBlockHandler = (e: KeyboardEvent) => {
if (isNaN(parseInt(e.key)) || e.key === '0') {
return
}
this.holdingIndex = parseInt(e.key) - 1
this.holdingBlock = this.holdingBlocks[this.holdingIndex] ?? BlockType.grass
}
wheelHandler = (e: WheelEvent) => {
if (!this.wheelGap) {
this.wheelGap = true
setTimeout(() => {
this.wheelGap = false
}, 100)
if (e.deltaY > 0) {
this.holdingIndex++
this.holdingIndex > 9 && (this.holdingIndex = 0)
} else if (e.deltaY < 0) {
this.holdingIndex--
this.holdingIndex < 0 && (this.holdingIndex = 9)
}
this.holdingBlock =
this.holdingBlocks[this.holdingIndex] ?? BlockType.grass
}
}
initEventListeners = () => {
// add / remove handler when pointer lock / unlock
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement) {
document.body.addEventListener(
'keydown',
this.changeHoldingBlockHandler
)
document.body.addEventListener('wheel', this.wheelHandler)
document.body.addEventListener('keydown', this.setMovementHandler)
document.body.addEventListener('keyup', this.resetMovementHandler)
document.body.addEventListener('mousedown', this.mousedownHandler)
document.body.addEventListener('mouseup', this.mouseupHandler)
} else {
document.body.removeEventListener(
'keydown',
this.changeHoldingBlockHandler
)
document.body.removeEventListener('wheel', this.wheelHandler)
document.body.removeEventListener('keydown', this.setMovementHandler)
document.body.removeEventListener('keyup', this.resetMovementHandler)
document.body.removeEventListener('mousedown', this.mousedownHandler)
document.body.removeEventListener('mouseup', this.mouseupHandler)
this.velocity = new THREE.Vector3(0, 0, 0)
}
})
}
// move along X with direction factor
moveX(distance: number, delta: number) {
this.camera.position.x +=
distance * (this.player.speed / Math.PI) * 2 * delta
}
// move along Z with direction factor
moveZ = (distance: number, delta: number) => {
this.camera.position.z +=
distance * (this.player.speed / Math.PI) * 2 * delta
}
// collide checking
collideCheckAll = (
position: THREE.Vector3,
noise: Noise,
customBlocks: Block[],
far: number
) => {
this.collideCheck(Side.down, position, noise, customBlocks, far)
this.collideCheck(Side.front, position, noise, customBlocks)
this.collideCheck(Side.back, position, noise, customBlocks)
this.collideCheck(Side.left, position, noise, customBlocks)
this.collideCheck(Side.right, position, noise, customBlocks)
this.collideCheck(Side.up, position, noise, customBlocks)
}
collideCheck = (
side: Side,
position: THREE.Vector3,
noise: Noise,
customBlocks: Block[],
far: number = this.player.body.width
) => {
const matrix = new THREE.Matrix4()
//reset simulation blocks
let index = 0
this.tempMesh.instanceMatrix = new THREE.InstancedBufferAttribute(
new Float32Array(100 * 16),
16
)
// block to remove
let removed = false
let treeRemoved = new Array<boolean>(
this.terrain.noise.treeHeight + 1
).fill(false)
// get block position
let x = Math.round(position.x)
let z = Math.round(position.z)
switch (side) {
case Side.front:
x++
this.raycasterFront.ray.origin = position
break
case Side.back:
x--
this.raycasterBack.ray.origin = position
break
case Side.left:
z--
this.raycasterLeft.ray.origin = position
break
case Side.right:
z++
this.raycasterRight.ray.origin = position
break
case Side.down:
this.raycasterDown.ray.origin = position
this.raycasterDown.far = far
break
case Side.up:
this.raycasterUp.ray.origin = new THREE.Vector3().copy(position)
this.raycasterUp.ray.origin.y--
break
}
let y =
Math.floor(
noise.get(x / noise.gap, z / noise.gap, noise.seed) * noise.amp
) + 30
// check custom blocks
for (const block of customBlocks) {
if (block.x === x && block.z === z) {
if (block.placed) {
// placed blocks
matrix.setPosition(block.x, block.y, block.z)
this.tempMesh.setMatrixAt(index++, matrix)
} else if (block.y === y) {
// removed blocks
removed = true
} else {
for (let i = 1; i <= this.terrain.noise.treeHeight; i++) {
if (block.y === y + i) {
treeRemoved[i] = true
}
}
}
}
}
// update simulation blocks (ignore removed blocks)
if (!removed) {
matrix.setPosition(x, y, z)
this.tempMesh.setMatrixAt(index++, matrix)
}
for (let i = 1; i <= this.terrain.noise.treeHeight; i++) {
if (!treeRemoved[i]) {
let treeOffset =
noise.get(x / noise.treeGap, z / noise.treeGap, noise.treeSeed) *
noise.treeAmp
let stoneOffset =
noise.get(x / noise.stoneGap, z / noise.stoneGap, noise.stoneSeed) *
noise.stoneAmp
if (
treeOffset > noise.treeThreshold &&
y >= 27 &&
stoneOffset < noise.stoneThreshold
) {
matrix.setPosition(x, y + i, z)
this.tempMesh.setMatrixAt(index++, matrix)
}
}
}
this.tempMesh.instanceMatrix.needsUpdate = true
// update collide
const origin = new THREE.Vector3(position.x, position.y - 1, position.z)
switch (side) {
case Side.front: {
const c1 = this.raycasterFront.intersectObject(this.tempMesh).length
this.raycasterFront.ray.origin = origin
const c2 = this.raycasterFront.intersectObject(this.tempMesh).length
c1 || c2 ? (this.frontCollide = true) : (this.frontCollide = false)
break
}
case Side.back: {
const c1 = this.raycasterBack.intersectObject(this.tempMesh).length
this.raycasterBack.ray.origin = origin
const c2 = this.raycasterBack.intersectObject(this.tempMesh).length
c1 || c2 ? (this.backCollide = true) : (this.backCollide = false)
break
}
case Side.left: {
const c1 = this.raycasterLeft.intersectObject(this.tempMesh).length
this.raycasterLeft.ray.origin = origin
const c2 = this.raycasterLeft.intersectObject(this.tempMesh).length
c1 || c2 ? (this.leftCollide = true) : (this.leftCollide = false)
break
}
case Side.right: {
const c1 = this.raycasterRight.intersectObject(this.tempMesh).length
this.raycasterRight.ray.origin = origin
const c2 = this.raycasterRight.intersectObject(this.tempMesh).length
c1 || c2 ? (this.rightCollide = true) : (this.rightCollide = false)
break
}
case Side.down: {
const c1 = this.raycasterDown.intersectObject(this.tempMesh).length
c1 ? (this.downCollide = true) : (this.downCollide = false)
break
}
case Side.up: {
const c1 = this.raycasterUp.intersectObject(this.tempMesh).length
c1 ? (this.upCollide = true) : (this.upCollide = false)
break
}
}
}
update = () => {
this.p1 = performance.now()
const delta = (this.p1 - this.p2) / 1000
if (
// dev mode
this.player.mode === Mode.flying
) {
this.control.moveForward(this.velocity.x * delta)
this.control.moveRight(this.velocity.z * delta)
this.camera.position.y += this.velocity.y * delta
} else {
// normal mode
this.collideCheckAll(
this.camera.position,
this.terrain.noise,
this.terrain.customBlocks,
this.far - this.velocity.y * delta
)
// gravity
if (Math.abs(this.velocity.y) < this.player.falling) {
this.velocity.y -= 25 * delta
}
// up collide handler
if (this.upCollide) {
this.velocity.y = -225 * delta
this.far = this.player.body.height
}
// down collide and jump handler
if (this.downCollide && !this.isJumping) {
this.velocity.y = 0
} else if (this.downCollide && this.isJumping) {
this.isJumping = false
}
// side collide handler
let vector = new THREE.Vector3(0, 0, -1).applyQuaternion(
this.camera.quaternion
)
let direction = Math.atan2(vector.x, vector.z)
if (
this.frontCollide ||
this.backCollide ||
this.leftCollide ||
this.rightCollide
) {
// collide front (positive x)
if (this.frontCollide) {
// camera front
if (direction < Math.PI && direction > 0 && this.velocity.x > 0) {
if (
(!this.leftCollide && direction > Math.PI / 2) ||
(!this.rightCollide && direction < Math.PI / 2)
) {
this.moveZ(Math.PI / 2 - direction, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera back
if (direction < 0 && direction > -Math.PI && this.velocity.x < 0) {
if (
(!this.leftCollide && direction > -Math.PI / 2) ||
(!this.rightCollide && direction < -Math.PI / 2)
) {
this.moveZ(-Math.PI / 2 - direction, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera left
if (
direction < Math.PI / 2 &&
direction > -Math.PI / 2 &&
this.velocity.z < 0
) {
if (
(!this.rightCollide && direction < 0) ||
(!this.leftCollide && direction > 0)
) {
this.moveZ(-direction, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
}
// camera right
if (
(direction < -Math.PI / 2 || direction > Math.PI / 2) &&
this.velocity.z > 0
) {
if (!this.rightCollide && direction > 0) {
this.moveZ(Math.PI - direction, delta)
}
if (!this.leftCollide && direction < 0) {
this.moveZ(-Math.PI - direction, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
}
}
// collide back (negative x)
if (this.backCollide) {
// camera front
if (direction < 0 && direction > -Math.PI && this.velocity.x > 0) {
if (
(!this.leftCollide && direction < -Math.PI / 2) ||
(!this.rightCollide && direction > -Math.PI / 2)
) {
this.moveZ(Math.PI / 2 + direction, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera back
if (direction < Math.PI && direction > 0 && this.velocity.x < 0) {
if (
(!this.leftCollide && direction < Math.PI / 2) ||
(!this.rightCollide && direction > Math.PI / 2)
) {
this.moveZ(direction - Math.PI / 2, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera left
if (
(direction < -Math.PI / 2 || direction > Math.PI / 2) &&
this.velocity.z < 0
) {
if (!this.leftCollide && direction > 0) {
this.moveZ(-Math.PI + direction, delta)
}
if (!this.rightCollide && direction < 0) {
this.moveZ(Math.PI + direction, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
}
// camera right
if (
direction < Math.PI / 2 &&
direction > -Math.PI / 2 &&
this.velocity.z > 0
) {
if (
(!this.leftCollide && direction < 0) ||
(!this.rightCollide && direction > 0)
) {
this.moveZ(direction, delta)
}
} else if (
!this.leftCollide &&
!this.rightCollide &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
}
}
// collide left (negative z)
if (this.leftCollide) {
// camera front
if (
(direction < -Math.PI / 2 || direction > Math.PI / 2) &&
this.velocity.x > 0
) {
if (!this.frontCollide && direction > 0) {
this.moveX(Math.PI - direction, delta)
}
if (!this.backCollide && direction < 0) {
this.moveX(-Math.PI - direction, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.frontCollide &&
direction < 0 &&
direction > -Math.PI / 2 &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.backCollide &&
direction < Math.PI / 2 &&
direction > 0 &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera back
if (
direction < Math.PI / 2 &&
direction > -Math.PI / 2 &&
this.velocity.x < 0
) {
if (
(!this.frontCollide && direction < 0) ||
(!this.backCollide && direction > 0)
) {
this.moveX(-direction, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.frontCollide &&
direction < Math.PI &&
direction > Math.PI / 2 &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.backCollide &&
direction > -Math.PI &&
direction < -Math.PI / 2 &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera left
if (direction > 0 && direction < Math.PI && this.velocity.z < 0) {
if (
(!this.backCollide && direction > Math.PI / 2) ||
(!this.frontCollide && direction < Math.PI / 2)
) {
this.moveX(Math.PI / 2 - direction, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.frontCollide &&
direction > -Math.PI &&
direction < -Math.PI / 2 &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.backCollide &&
direction > -Math.PI / 2 &&
direction < 0 &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
}
// camera right
if (direction < 0 && direction > -Math.PI && this.velocity.z > 0) {
if (
(!this.backCollide && direction > -Math.PI / 2) ||
(!this.frontCollide && direction < -Math.PI / 2)
) {
this.moveX(-Math.PI / 2 - direction, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.frontCollide &&
direction < Math.PI / 2 &&
direction > 0 &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.backCollide &&
direction < Math.PI &&
direction > Math.PI / 2 &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
}
}
// collide right (positive z)
if (this.rightCollide) {
// camera front
if (
direction < Math.PI / 2 &&
direction > -Math.PI / 2 &&
this.velocity.x > 0
) {
if (
(!this.backCollide && direction < 0) ||
(!this.frontCollide && direction > 0)
) {
this.moveX(direction, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.frontCollide &&
direction < -Math.PI / 2 &&
direction > -Math.PI &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.backCollide &&
direction < Math.PI &&
direction > Math.PI / 2 &&
this.velocity.x > 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera back
if (
(direction < -Math.PI / 2 || direction > Math.PI / 2) &&
this.velocity.x < 0
) {
if (!this.backCollide && direction > 0) {
this.moveX(-Math.PI + direction, delta)
}
if (!this.frontCollide && direction < 0) {
this.moveX(Math.PI + direction, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.frontCollide &&
direction < Math.PI / 2 &&
direction > 0 &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
} else if (
this.backCollide &&
direction < 0 &&
direction > -Math.PI / 2 &&
this.velocity.x < 0
) {
this.control.moveForward(this.velocity.x * delta)
}
// camera left
if (direction < 0 && direction > -Math.PI && this.velocity.z < 0) {
if (
(!this.frontCollide && direction > -Math.PI / 2) ||
(!this.backCollide && direction < -Math.PI / 2)
) {
this.moveX(Math.PI / 2 + direction, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.frontCollide &&
direction > Math.PI / 2 &&
direction < Math.PI &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.backCollide &&
direction > 0 &&
direction < Math.PI / 2 &&
this.velocity.z < 0
) {
this.control.moveRight(this.velocity.z * delta)
}
// camera right
if (direction > 0 && direction < Math.PI && this.velocity.z > 0) {
if (
(!this.frontCollide && direction > Math.PI / 2) ||
(!this.backCollide && direction < Math.PI / 2)
) {
this.moveX(direction - Math.PI / 2, delta)
}
} else if (
!this.frontCollide &&
!this.backCollide &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.frontCollide &&
direction > -Math.PI / 2 &&
direction < 0 &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
} else if (
this.backCollide &&
direction > -Math.PI &&
direction < -Math.PI / 2 &&
this.velocity.z > 0
) {
this.control.moveRight(this.velocity.z * delta)
}
}
} else {
// no collide
this.control.moveForward(this.velocity.x * delta)
this.control.moveRight(this.velocity.z * delta)
}
this.camera.position.y += this.velocity.y * delta
// catching net
if (this.camera.position.y < -100) {
this.camera.position.y = 60
}
}
this.p2 = this.p1
}
}
//legacy
import * as THREE from 'three'
import Block from '../../terrain/mesh/block'
import { ImprovedNoise } from 'three/examples/jsm/math/ImprovedNoise'
const noise = new ImprovedNoise()
const raycaster = new THREE.Raycaster(
new THREE.Vector3(),
new THREE.Vector3(0, -1, 0),
0,
1.8
)
onmessage = (
msg: MessageEvent<{
position: THREE.Vector3
far: number
blocks: Block[]
seed: number
noiseGap: number
noiseAmp: number
}>
) => {
raycaster.ray.origin = new THREE.Vector3(
msg.data.position.x,
msg.data.position.y,
msg.data.position.z
)
raycaster.far = msg.data.far
let index = 0
const mesh = new THREE.InstancedMesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial(),
100
)
const matrix = new THREE.Matrix4()
let x = Math.round(msg.data.position.x)
let z = Math.round(msg.data.position.z)
let y =
Math.floor(
noise.noise(x / msg.data.noiseGap, z / msg.data.noiseGap, msg.data.seed) *
msg.data.noiseAmp
) + 30
let removed = false
for (const block of msg.data.blocks) {
if (
block.x === Math.round(msg.data.position.x) &&
block.z === Math.round(msg.data.position.z)
) {
if (block.placed) {
matrix.setPosition(new THREE.Vector3(block.x, block.y, block.z))
mesh.setMatrixAt(index++, matrix)
} else if (block.y === y) {
removed = true
}
}
}
if (!removed) {
matrix.setPosition(new THREE.Vector3(x, y, z))
mesh.setMatrixAt(index++, matrix)
}
mesh.instanceMatrix.needsUpdate = true
if (raycaster.intersectObject(mesh).length) {
postMessage(true)
} else {
postMessage(false)
}
}
//legacy
import * as THREE from 'three'
onmessage = (
msg: MessageEvent<{
count: number
matrices: THREE.InstancedBufferAttribute[]
position: THREE.Vector3
}>
) => {
let meshes = []
let raycaster = new THREE.Raycaster(
new THREE.Vector3(
msg.data.position.x,
msg.data.position.y - 1,
msg.data.position.z
),
new THREE.Vector3(1, 0, 0),
0,
0.6
)
for (let matrix of msg.data.matrices) {
let mesh = new THREE.InstancedMesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial(),
msg.data.count
)
mesh.instanceMatrix = matrix
meshes.push(mesh)
}
if (raycaster.intersectObjects(meshes).length) {
postMessage(true)
} else {
postMessage(false)
}
}
import * as THREE from 'three'
export default class Core {
constructor() {
this.camera = new THREE.PerspectiveCamera()
this.renderer = new THREE.WebGLRenderer()
this.scene = new THREE.Scene()
this.initScene()
this.initRenderer()
this.initCamera()
}
camera: THREE.PerspectiveCamera
scene: THREE.Scene
renderer: THREE.Renderer
initCamera = () => {
this.camera.fov = 50
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.near = 0.01
this.camera.far = 500
this.camera.updateProjectionMatrix()
this.camera.position.set(8, 50, 8)
this.camera.lookAt(100, 30, 100)
window.addEventListener('resize', () => {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
})
}
initScene = () => {
this.scene = new THREE.Scene()
const backgroundColor = 0x87ceeb
this.scene.fog = new THREE.Fog(backgroundColor, 1, 96)
this.scene.background = new THREE.Color(backgroundColor)
const sunLight = new THREE.PointLight(0xffffff, 0.5)
sunLight.position.set(500, 500, 500)
this.scene.add(sunLight)
const sunLight2 = new THREE.PointLight(0xffffff, 0.2)
sunLight2.position.set(-500, 500, -500)
this.scene.add(sunLight2)
const reflectionLight = new THREE.AmbientLight(0x404040)
this.scene.add(reflectionLight)
}
initRenderer = () => {
this.renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(this.renderer.domElement)
window.addEventListener('resize', () => {
this.renderer.setSize(window.innerWidth, window.innerHeight)
})
}
}
import Core from './core'
import Control from './control'
import Player from './player'
import Terrain from './terrain'
import UI from './ui'
import Audio from './audio'
import './style.css'
const core = new Core()
const camera = core.camera
const scene = core.scene
const renderer = core.renderer
const player = new Player()
const audio = new Audio(camera)
const terrain = new Terrain(scene, camera)
const control = new Control(scene, camera, player, terrain, audio)
const ui = new UI(terrain, control)
// animation
;(function animate() {
// let p1 = performance.now()
requestAnimationFrame(animate)
control.update()
terrain.update()
ui.update()
renderer.render(scene, camera)
// console.log(performance.now()-p1)
})()
export enum Mode {
walking = 'walking',
sprinting = 'sprinting',
flying = 'flying',
sprintFlying = 'sprintFlying',
sneaking = 'sneaking'
}
export enum Speed {
// walking = 4.317,
walking = 5.612,
sprinting = 5.612,
// flying = 10.89,
flying = 21.78,
sprintFlying = 21.78,
sneaking = 1.95
}
export default class Player {
mode = Mode.walking
speed = Speed[this.mode]
setMode(Mode: Mode) {
this.mode = Mode
this.speed = Speed[this.mode]
}
falling = 38.4
jump = 1.2522
body = {
height: 1.8,
width: 0.5
}
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="50" height="50" viewBox="0 0 250 250" ><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg>
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"frametime": 8,
"interpolate": true,
"frames": [
0,
1,
2
]
}
}
{
"animation": {
"frametime": 300,
"interpolate": true,
"frames": [
0,
1,
0,
2,
0,
3,
0,
1,
2,
1,
3,
1,
0,
2,
1,
2,
3,
2,
0,
3,
1,
3
]
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
{
"animation": {
"interpolate": true,
"frametime": 10
}
}
body {
font-family: Minecraft, Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
overflow: hidden;
}
* {
touch-action: none;
}
@font-face {
font-family: Minecraft;
src: url(/src/static/mc-font.otf);
font-display: swap;
}
.start {
background-image: url(/src/static/menu.jpeg);
background-repeat: no-repeat;
background-size: 100% 100%;
}
.start #exit {
display: none;
}
.menu {
z-index: 1;
position: fixed;
left: 0%;
top: 0%;
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 75vh;
padding-top: 25vh;
text-align: center;
user-select: none;
background-color: hsla(0, 0%, 11%, 0.7);
}
.button {
width: 300px;
margin-bottom: 15px;
padding: 10px;
display: inline-block;
font-family: Minecraft, Avenir, Helvetica, Arial, sans-serif;
font-size: 1.2rem;
color: white;
background-color: #727272;
cursor: pointer;
border-left: 2px solid #a4a4a4;
border-top: 2px solid #a4a4a4;
border-bottom: 2px solid #545655;
border-right: 2px solid #545655;
box-shadow: 0 0 0 2px black;
}
.button:hover {
background-color: #218306;
border-left: 2px solid #17cd07;
border-top: 2px solid #17cd07;
border-bottom: 2px solid #004e00;
border-right: 2px solid #004e00;
box-shadow: 0 0 0 2px white;
}
.footer {
position: fixed;
color: white;
left: 50%;
top: 100%;
opacity: 75%;
transform: translate(-50%, -100%);
font-family: sans-serif;
}
.fps {
position: fixed;
color: white;
left: 0%;
top: 0%;
padding: 5px;
margin: 1px;
border: 2px solid rgb(141, 139, 139);
background-color: hsla(0, 0%, 11%, 0.5);
}
.bag {
position: fixed;
left: 50%;
top: 100%;
transform: translate(-50%, -100%);
width: fit-content;
background-color: hsla(0, 0%, 11%, 0.5);
height: 62px;
}
.item {
display: inline-block;
width: 56px;
height: 56px;
margin: 1px;
border: 2px solid rgb(141, 139, 139);
}
.selected {
border: 2px solid rgb(236, 234, 234);
outline: 1px solid rgb(236, 234, 234);
}
.icon {
display: block;
margin: auto;
transform: translate(0, 20%);
width: 40px;
height: 40px;
}
.cross-hair {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
user-select: none;
font-size: 3rem;
color: white;
}
.hidden {
display: none;
}
.features {
z-index: 2;
position: fixed;
left: 50%;
top: 20%;
transform: translate(-50%, 0%);
width: 300px;
color: white;
background-color: #727272;
border-left: 2px solid #a4a4a4;
border-top: 2px solid #a4a4a4;
border-bottom: 2px solid #545655;
border-right: 2px solid #545655;
box-shadow: 0 0 0 2px black;
padding: 0px 30px 0px 30px;
text-align: center;
user-select: none;
text-align: left;
line-height: 1.8rem;
}
.save-modal,
.load-modal {
z-index: 3;
position: fixed;
left: 50%;
transform: translate(-50%, 0%);
margin-top: calc(25vh + 62px);
padding: 12px 0;
width: 300px;
color: white;
background-color: #218306;
border-left: 2px solid #17cd07;
border-top: 2px solid #17cd07;
border-bottom: 2px solid #004e00;
border-right: 2px solid #004e00;
box-shadow: 0 0 0 2px white;
opacity: 0%;
transition: opacity 0.35s ease-in-out;
text-align: center;
user-select: none;
}
.load-modal {
background-color: #727272;
border-left: 2px solid #a4a4a4;
border-top: 2px solid #a4a4a4;
border-bottom: 2px solid #545655;
border-right: 2px solid #545655;
box-shadow: 0 0 0 2px black;
}
.show {
transition: opacity 0.5s ease-in-out;
opacity: 100%;
}
.github svg {
z-index: 3;
fill: #727272;
position: fixed;
left: 100%;
top: -1%;
transform: translate(-99%, 0%) rotate(-90deg) scale(1, -1);
}
.settings {
z-index: 2;
position: fixed;
left: 50%;
top: 20%;
transform: translate(-50%, 0%);
width: 300px;
height: 370px;
color: white;
background-color: #727272;
border-left: 2px solid #a4a4a4;
border-top: 2px solid #a4a4a4;
border-bottom: 2px solid #545655;
border-right: 2px solid #545655;
box-shadow: 0 0 0 2px black;
padding: 0px 30px 0px 30px;
text-align: center;
user-select: none;
text-align: left;
line-height: 1.8rem;
}
#distance-input,
#fov-input {
appearance: none;
width: 100%;
height: 16px;
background-color: #dddddd;
box-shadow: 0 0 0 2px black;
}
#music-input {
appearance: none;
width: 14%;
height: 16px;
background-color: #dddddd;
box-shadow: 0 0 0 2px black;
}
#distance-input::-webkit-slider-thumb:hover,
#fov-input::-webkit-slider-thumb:hover,
#music-input::-webkit-slider-thumb:hover {
background-color: #218306;
border-left: 2px solid #17cd07;
border-top: 2px solid #17cd07;
border-bottom: 2px solid #004e00;
border-right: 2px solid #004e00;
box-shadow: 0 0 0 2px white;
}
#distance-input::-webkit-slider-thumb,
#fov-input::-webkit-slider-thumb,
#music-input::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 30px;
background-color: #727272;
border-left: 2px solid #a4a4a4;
border-top: 2px solid #a4a4a4;
border-bottom: 2px solid #545655;
border-right: 2px solid #545655;
box-shadow: 0 0 0 2px black;
cursor: pointer;
}
#distance-input::-moz-range-thumb,
#fov-input::-moz-range-thumb,
#music-input::-moz-range-thumb {
appearance: none;
width: 20px;
height: 30px;
background-color: #727272;
border-left: 2px solid #a4a4a4;
border-top: 2px solid #a4a4a4;
border-bottom: 2px solid #545655;
border-right: 2px solid #545655;
box-shadow: 0 0 0 2px black;
cursor: pointer;
}
* {
-webkit-touch-callout: none;
-moz-touch-callout: none;
-ms-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
import * as THREE from 'three'
import Terrain from '..'
/**
* Highlight block on crosshair
*/
export default class BlockHighlight {
constructor(
scene: THREE.Scene,
camera: THREE.PerspectiveCamera,
terrain: Terrain
) {
this.camera = camera
this.scene = scene
this.terrain = terrain
this.raycaster = new THREE.Raycaster()
this.raycaster.far = 8
}
scene: THREE.Scene
camera: THREE.PerspectiveCamera
terrain: Terrain
raycaster: THREE.Raycaster
block: THREE.Intersection | null = null
// highlight block mesh
geometry = new THREE.BoxGeometry(1.01, 1.01, 1.01)
material = new THREE.MeshStandardMaterial({
transparent: true,
opacity: 0.25
// depthWrite: false
})
mesh = new THREE.Mesh(new THREE.BoxGeometry(), this.material)
// block simulation
index = 0
instanceMesh = new THREE.InstancedMesh(
new THREE.BoxGeometry(),
new THREE.MeshBasicMaterial(),
1000
)
update() {
// remove last highlight and reset block simulation
this.scene.remove(this.mesh)
this.index = 0
this.instanceMesh.instanceMatrix = new THREE.InstancedBufferAttribute(
new Float32Array(1000 * 16),
16
)
const position = this.camera.position
const matrix = new THREE.Matrix4()
const idMap = new Map<string, number>()
const noise = this.terrain.noise
let xPos = Math.round(position.x)
let zPos = Math.round(position.z)
for (let i = -8; i < 8; i++) {
for (let j = -8; j < 8; j++) {
// check terrain
let x = xPos + i
let z = zPos + j
let y =
Math.floor(
noise.get(x / noise.gap, z / noise.gap, noise.seed) * noise.amp
) + 30
idMap.set(`${x}_${y}_${z}`, this.index)
matrix.setPosition(x, y, z)
this.instanceMesh.setMatrixAt(this.index++, matrix)
let stoneOffset =
noise.get(x / noise.stoneGap, z / noise.stoneGap, noise.stoneSeed) *
noise.stoneAmp
let treeOffset =
noise.get(x / noise.treeGap, z / noise.treeGap, noise.treeSeed) *
noise.treeAmp
// check tree
if (
treeOffset > noise.treeThreshold &&
y - 30 >= -3 &&
stoneOffset < noise.stoneThreshold
) {
for (let t = 1; t <= noise.treeHeight; t++) {
idMap.set(`${x}_${y + t}_${z}`, this.index)
matrix.setPosition(x, y + t, z)
this.instanceMesh.setMatrixAt(this.index++, matrix)
}
// leaf
// for (let i = -3; i < 3; i++) {
// for (let j = -3; j < 3; j++) {
// for (let k = -3; k < 3; k++) {
// if (i === 0 && k === 0) {
// continue
// }
// let leafOffset =
// noise.get(
// (x + i + j) / noise.leafGap,
// (z + k) / noise.leafGap,
// noise.leafSeed
// ) * noise.leafAmp
// if (leafOffset > noise.leafThreshold) {
// idMap.set(
// `${x + i}_${y + noise.treeHeight + j}_${z + k}`,
// this.index
// )
// matrix.setPosition(x + i, y + noise.treeHeight + j, z + k)
// this.instanceMesh.setMatrixAt(this.index++, matrix)
// }
// }
// }
// }
}
}
}
// check custom blocks
for (const block of this.terrain.customBlocks) {
if (block.placed) {
matrix.setPosition(block.x, block.y, block.z)
this.instanceMesh.setMatrixAt(this.index++, matrix)
} else {
if (idMap.has(`${block.x}_${block.y}_${block.z}`)) {
let id = idMap.get(`${block.x}_${block.y}_${block.z}`)
this.instanceMesh.setMatrixAt(
id!,
new THREE.Matrix4().set(
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
)
)
}
}
}
// highlight new block
this.raycaster.setFromCamera({ x: 0, y: 0 }, this.camera)
this.block = this.raycaster.intersectObject(this.instanceMesh)[0]
if (
this.block &&
this.block.object instanceof THREE.InstancedMesh &&
typeof this.block.instanceId === 'number'
) {
this.mesh = new THREE.Mesh(this.geometry, this.material)
let matrix = new THREE.Matrix4()
this.block.object.getMatrixAt(this.block.instanceId, matrix)
const position = new THREE.Vector3().setFromMatrixPosition(matrix)
this.mesh.position.set(position.x, position.y, position.z)
this.scene.add(this.mesh)
}
}
}
import * as THREE from 'three'
import Materials, { MaterialType } from './mesh/materials'
import Block from './mesh/block'
import Highlight from './highlight'
import Noise from './noise'
import Generate from './worker/generate?worker'
export enum BlockType {
grass = 0,
sand = 1,
tree = 2,
leaf = 3,
dirt = 4,
stone = 5,
coal = 6,
wood = 7,
diamond = 8,
quartz = 9,
glass = 10,
bedrock = 11
}
export default class Terrain {
constructor(scene: THREE.Scene, camera: THREE.PerspectiveCamera) {
this.scene = scene
this.camera = camera
this.maxCount =
(this.distance * this.chunkSize * 2 + this.chunkSize) ** 2 + 500
this.highlight = new Highlight(scene, camera, this)
this.scene.add(this.cloud)
// generate worker callback handler
this.generateWorker.onmessage = (
msg: MessageEvent<{
idMap: Map<string, number>
arrays: ArrayLike<number>[]
blocksCount: number[]
}>
) => {
this.resetBlocks()
this.idMap = msg.data.idMap
this.blocksCount = msg.data.blocksCount
for (let i = 0; i < msg.data.arrays.length; i++) {
this.blocks[i].instanceMatrix = new THREE.InstancedBufferAttribute(
(this.blocks[i].instanceMatrix.array = msg.data.arrays[i]),
16
)
}
for (const block of this.blocks) {
block.instanceMatrix.needsUpdate = true
}
}
}
// core properties
scene: THREE.Scene
camera: THREE.PerspectiveCamera
distance = 3
chunkSize = 24
// terrain properties
maxCount: number
chunk = new THREE.Vector2(0, 0)
previousChunk = new THREE.Vector2(0, 0)
noise = new Noise()
// materials
materials = new Materials()
materialType = [
MaterialType.grass,
MaterialType.sand,
MaterialType.tree,
MaterialType.leaf,
MaterialType.dirt,
MaterialType.stone,
MaterialType.coal,
MaterialType.wood,
MaterialType.diamond,
MaterialType.quartz,
MaterialType.glass,
MaterialType.bedrock
]
// other properties
blocks: THREE.InstancedMesh[] = []
blocksCount: number[] = []
blocksFactor = [1, 0.2, 0.1, 0.7, 0.1, 0.2, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
customBlocks: Block[] = []
highlight: Highlight
idMap = new Map<string, number>()
generateWorker = new Generate()
// cloud
cloud = new THREE.InstancedMesh(
new THREE.BoxGeometry(20, 5, 14),
new THREE.MeshStandardMaterial({
transparent: true,
color: 0xffffff,
opacity: 0.4
}),
1000
)
cloudCount = 0
cloudGap = 5
getCount = (type: BlockType) => {
return this.blocksCount[type]
}
setCount = (type: BlockType) => {
this.blocksCount[type] = this.blocksCount[type] + 1
}
initBlocks = () => {
// reset
for (const block of this.blocks) {
this.scene.remove(block)
}
this.blocks = []
// create instance meshes
const geometry = new THREE.BoxGeometry()
for (let i = 0; i < this.materialType.length; i++) {
let block = new THREE.InstancedMesh(
geometry,
this.materials.get(this.materialType[i]),
this.maxCount * this.blocksFactor[i]
)
block.name = BlockType[i]
this.blocks.push(block)
this.scene.add(block)
}
this.blocksCount = new Array(this.materialType.length).fill(0)
}
resetBlocks = () => {
// reest count and instance matrix
for (let i = 0; i < this.blocks.length; i++) {
this.blocks[i].instanceMatrix = new THREE.InstancedBufferAttribute(
new Float32Array(this.maxCount * this.blocksFactor[i] * 16),
16
)
}
}
generate = () => {
this.blocksCount = new Array(this.blocks.length).fill(0)
// post work to generate worker
this.generateWorker.postMessage({
distance: this.distance,
chunk: this.chunk,
noiseSeed: this.noise.seed,
treeSeed: this.noise.treeSeed,
stoneSeed: this.noise.stoneSeed,
coalSeed: this.noise.coalSeed,
idMap: new Map<string, number>(),
blocksFactor: this.blocksFactor,
blocksCount: this.blocksCount,
customBlocks: this.customBlocks,
chunkSize: this.chunkSize
})
// cloud
if (this.cloudGap++ > 5) {
this.cloudGap = 0
this.cloud.instanceMatrix = new THREE.InstancedBufferAttribute(
new Float32Array(1000 * 16),
16
)
this.cloudCount = 0
for (
let x =
-this.chunkSize * this.distance * 3 + this.chunkSize * this.chunk.x;
x <
this.chunkSize * this.distance * 3 +
this.chunkSize +
this.chunkSize * this.chunk.x;
x += 20
) {
for (
let z =
-this.chunkSize * this.distance * 3 + this.chunkSize * this.chunk.y;
z <
this.chunkSize * this.distance * 3 +
this.chunkSize +
this.chunkSize * this.chunk.y;
z += 20
) {
const matrix = new THREE.Matrix4()
matrix.setPosition(x, 80 + (Math.random() - 0.5) * 30, z)
if (Math.random() > 0.8) {
this.cloud.setMatrixAt(this.cloudCount++, matrix)
}
}
}
this.cloud.instanceMatrix.needsUpdate = true
}
}
// generate adjacent blocks after removing a block (vertical infinity world)
generateAdjacentBlocks = (position: THREE.Vector3) => {
const { x, y, z } = position
const noise = this.noise
const yOffset = Math.floor(
noise.get(x / noise.gap, z / noise.gap, noise.seed) * noise.amp
)
if (y > 30 + yOffset) {
return
}
const stoneOffset =
noise.get(x / noise.stoneGap, z / noise.stoneGap, noise.stoneSeed) *
noise.stoneAmp
let type: BlockType
if (stoneOffset > noise.stoneThreshold || y < 23) {
type = BlockType.stone
} else {
if (yOffset < -3) {
type = BlockType.sand
} else {
type = BlockType.dirt
}
}
this.buildBlock(new THREE.Vector3(x, y - 1, z), type)
this.buildBlock(new THREE.Vector3(x, y + 1, z), type)
this.buildBlock(new THREE.Vector3(x - 1, y, z), type)
this.buildBlock(new THREE.Vector3(x + 1, y, z), type)
this.buildBlock(new THREE.Vector3(x, y, z - 1), type)
this.buildBlock(new THREE.Vector3(x, y, z + 1), type)
this.blocks[type].instanceMatrix.needsUpdate = true
}
buildBlock = (position: THREE.Vector3, type: BlockType) => {
const noise = this.noise
// check if it's natural terrain
const yOffset = Math.floor(
noise.get(position.x / noise.gap, position.z / noise.gap, noise.seed) *
noise.amp
)
if (position.y >= 30 + yOffset || position.y < 0) {
return
}
position.y === 0 && (type = BlockType.bedrock)
// check custom blocks
for (const block of this.customBlocks) {
if (
block.x === position.x &&
block.y === position.y &&
block.z === position.z
) {
return
}
}
// build block
this.customBlocks.push(
new Block(position.x, position.y, position.z, type, true)
)
const matrix = new THREE.Matrix4()
matrix.setPosition(position)
this.blocks[type].setMatrixAt(this.getCount(type), matrix)
this.blocks[type].instanceMatrix.needsUpdate = true
this.setCount(type)
}
update = () => {
this.chunk.set(
Math.floor(this.camera.position.x / this.chunkSize),
Math.floor(this.camera.position.z / this.chunkSize)
)
//generate terrain when getting into new chunk
if (
this.chunk.x !== this.previousChunk.x ||
this.chunk.y !== this.previousChunk.y
) {
this.generate()
}
this.previousChunk.copy(this.chunk)
this.highlight.update()
}
}
import { BlockType } from '../index'
/**
* Custom block
*/
export default class Block {
object: any
constructor(
x: number,
y: number,
z: number,
type: BlockType,
placed: boolean
) {
this.x = x
this.y = y
this.z = z
this.type = type
this.placed = placed
}
x: number
y: number
z: number
type: BlockType
placed: boolean
}
import * as THREE from 'three'
import stone from '../../static/textures/block/stone.png'
import coal_ore from '../../static/textures/block/coal_ore.png'
import iron_ore from '../../static/textures/block/iron_ore.png'
import grass_side from '../../static/textures/block/grass_block_side.png'
import grass_top_green from '../../static/textures/block/grass_top_green.png'
import dirt from '../../static/textures/block/dirt.png'
import oak_log from '../../static/textures/block/oak_log.png'
import oak_log_top from '../../static/textures/block/oak_log_top.png'
import oak_leaves from '../../static/textures/block/oak_leaves.png'
import sand from '../../static/textures/block/sand.png'
// import water from '../../static/textures/block/water.png'
import oak_wood from '../../static/textures/block/oak_planks.png'
import diamond from '../../static/textures/block/diamond_block.png'
import quartz from '../../static/textures/block/quartz_block_side.png'
import glass from '../../static/textures/block/glass.png'
import bedrock from '../../static/textures/block/bedrock.png'
export enum MaterialType {
grass = 'grass',
dirt = 'dirt',
tree = 'tree',
leaf = 'leaf',
sand = 'sand',
// water = 'water',
stone = 'stone',
coal = 'coal',
wood = 'wood',
diamond = 'diamond',
quartz = 'quartz',
glass = 'glass',
bedrock = 'bedrock'
}
let loader = new THREE.TextureLoader()
// load texture
const grassTopMaterial = loader.load(grass_top_green)
const grassMaterial = loader.load(grass_side)
const treeMaterial = loader.load(oak_log)
const treeTopMaterial = loader.load(oak_log_top)
const dirtMaterial = loader.load(dirt)
const stoneMaterial = loader.load(stone)
const coalMaterial = loader.load(coal_ore)
const ironMaterial = loader.load(iron_ore)
const leafMaterial = loader.load(oak_leaves)
const sandMaterial = loader.load(sand)
// const waterMaterial = loader.load(water)
const woodMaterial = loader.load(oak_wood)
const diamondMaterial = loader.load(diamond)
const quartzMaterial = loader.load(quartz)
const glassMaterial = loader.load(glass)
const bedrockMaterial = loader.load(bedrock)
// pixelate texture
grassTopMaterial.magFilter = THREE.NearestFilter
grassMaterial.magFilter = THREE.NearestFilter
treeMaterial.magFilter = THREE.NearestFilter
treeTopMaterial.magFilter = THREE.NearestFilter
dirtMaterial.magFilter = THREE.NearestFilter
stoneMaterial.magFilter = THREE.NearestFilter
coalMaterial.magFilter = THREE.NearestFilter
ironMaterial.magFilter = THREE.NearestFilter
leafMaterial.magFilter = THREE.NearestFilter
sandMaterial.magFilter = THREE.NearestFilter
// waterMaterial.magFilter = THREE.NearestFilter
woodMaterial.magFilter = THREE.NearestFilter
diamondMaterial.magFilter = THREE.NearestFilter
quartzMaterial.magFilter = THREE.NearestFilter
glassMaterial.magFilter = THREE.NearestFilter
bedrockMaterial.magFilter = THREE.NearestFilter
export default class Materials {
materials = {
grass: [
new THREE.MeshStandardMaterial({ map: grassMaterial }),
new THREE.MeshStandardMaterial({ map: grassMaterial }),
new THREE.MeshStandardMaterial({
map: grassTopMaterial
}),
new THREE.MeshStandardMaterial({ map: dirtMaterial }),
new THREE.MeshStandardMaterial({ map: grassMaterial }),
new THREE.MeshStandardMaterial({ map: grassMaterial })
],
dirt: new THREE.MeshStandardMaterial({ map: dirtMaterial }),
sand: new THREE.MeshStandardMaterial({ map: sandMaterial }),
tree: [
new THREE.MeshStandardMaterial({ map: treeMaterial }),
new THREE.MeshStandardMaterial({ map: treeMaterial }),
new THREE.MeshStandardMaterial({ map: treeTopMaterial }),
new THREE.MeshStandardMaterial({ map: treeTopMaterial }),
new THREE.MeshStandardMaterial({ map: treeMaterial }),
new THREE.MeshStandardMaterial({ map: treeMaterial })
],
leaf: new THREE.MeshStandardMaterial({
map: leafMaterial,
color: new THREE.Color(0, 1, 0),
transparent: true
}),
// water: new THREE.MeshStandardMaterial({
// map: waterMaterial,
// transparent: true,
// opacity: 0.7
// }),
stone: new THREE.MeshStandardMaterial({ map: stoneMaterial }),
coal: new THREE.MeshStandardMaterial({ map: coalMaterial }),
wood: new THREE.MeshStandardMaterial({ map: woodMaterial }),
diamond: new THREE.MeshStandardMaterial({ map: diamondMaterial }),
quartz: new THREE.MeshStandardMaterial({ map: quartzMaterial }),
glass: new THREE.MeshStandardMaterial({
map: glassMaterial,
transparent: true
}),
bedrock: new THREE.MeshStandardMaterial({ map: bedrockMaterial })
}
get = (
type: MaterialType
): THREE.MeshStandardMaterial | THREE.MeshStandardMaterial[] => {
return this.materials[type]
}
}
import { ImprovedNoise } from 'three/examples/jsm/math/ImprovedNoise'
// import { SimplexNoise } from 'three/examples/jsm/math/SimplexNoise'
export default class Noise {
noise = new ImprovedNoise()
seed = Math.random()
gap = 22
amp = 8
stoneSeed = this.seed * 0.4
stoneGap = 12
stoneAmp = 8
stoneThreshold = 3.5
coalSeed = this.seed * 0.5
coalGap = 3
coalAmp = 8
coalThreshold = 3
treeSeed = this.seed * 0.7
treeGap = 2
treeAmp = 6
treeHeight = 10
treeThreshold = 4
leafSeed = this.seed * 0.8
leafGap = 2
leafAmp = 5
leafThreshold = -0.03
get = (x: number, y: number, z: number) => {
return this.noise.noise(x, y, z)
}
}
import * as THREE from 'three'
import Block from '../mesh/block'
import Noise from '../noise'
enum BlockType {
grass = 0,
sand = 1,
tree = 2,
leaf = 3,
dirt = 4,
stone = 5,
coal = 6,
wood = 7,
diamond = 8,
quartz = 9,
glass = 10,
bedrock = 11
}
const matrix = new THREE.Matrix4()
const noise = new Noise()
const blocks: THREE.InstancedMesh[] = []
const geometry = new THREE.BoxGeometry()
let isFirstRun = true
onmessage = (
msg: MessageEvent<{
distance: number
chunk: THREE.Vector2
noiseSeed: number
treeSeed: number
stoneSeed: number
coalSeed: number
idMap: Map<string, number>
blocksFactor: number[]
blocksCount: number[]
customBlocks: Block[]
chunkSize: number
}>
) => {
// let p1 = performance.now()
const {
distance,
chunk,
noiseSeed,
idMap,
blocksFactor,
treeSeed,
stoneSeed,
coalSeed,
customBlocks,
blocksCount,
chunkSize
} = msg.data
const maxCount = (distance * chunkSize * 2 + chunkSize) ** 2 + 500
if (isFirstRun) {
for (let i = 0; i < blocksCount.length; i++) {
let block = new THREE.InstancedMesh(
geometry,
new THREE.MeshBasicMaterial(),
maxCount * blocksFactor[i]
)
blocks.push(block)
}
isFirstRun = false
}
noise.seed = noiseSeed
noise.treeSeed = treeSeed
noise.stoneSeed = stoneSeed
noise.coalSeed = coalSeed
for (let i = 0; i < blocks.length; i++) {
blocks[i].instanceMatrix = new THREE.InstancedBufferAttribute(
new Float32Array(maxCount * blocksFactor[i] * 16),
16
)
}
for (
let x = -chunkSize * distance + chunkSize * chunk.x;
x < chunkSize * distance + chunkSize + chunkSize * chunk.x;
x++
) {
for (
let z = -chunkSize * distance + chunkSize * chunk.y;
z < chunkSize * distance + chunkSize + chunkSize * chunk.y;
z++
) {
const y = 30
const yOffset = Math.floor(
noise.get(x / noise.gap, z / noise.gap, noise.seed) * noise.amp
)
matrix.setPosition(x, y + yOffset, z)
const stoneOffset =
noise.get(x / noise.stoneGap, z / noise.stoneGap, noise.stoneSeed) *
noise.stoneAmp
const coalOffset =
noise.get(x / noise.coalGap, z / noise.coalGap, noise.coalSeed) *
noise.coalAmp
if (stoneOffset > noise.stoneThreshold) {
if (coalOffset > noise.coalThreshold) {
// coal
idMap.set(`${x}_${y + yOffset}_${z}`, blocksCount[BlockType.coal])
blocks[BlockType.coal].setMatrixAt(
blocksCount[BlockType.coal]++,
matrix
)
} else {
// stone
idMap.set(`${x}_${y + yOffset}_${z}`, blocksCount[BlockType.stone])
blocks[BlockType.stone].setMatrixAt(
blocksCount[BlockType.stone]++,
matrix
)
}
} else {
if (yOffset < -3) {
// sand
idMap.set(`${x}_${y + yOffset}_${z}`, blocksCount[BlockType.sand])
blocks[BlockType.sand].setMatrixAt(
blocksCount[BlockType.sand]++,
matrix
)
} else {
// grass
idMap.set(`${x}_${y + yOffset}_${z}`, blocksCount[BlockType.grass])
blocks[BlockType.grass].setMatrixAt(
blocksCount[BlockType.grass]++,
matrix
)
}
}
// tree
const treeOffset =
noise.get(x / noise.treeGap, z / noise.treeGap, noise.treeSeed) *
noise.treeAmp
if (
treeOffset > noise.treeThreshold &&
yOffset >= -3 &&
stoneOffset < noise.stoneThreshold
) {
for (let i = 1; i <= noise.treeHeight; i++) {
idMap.set(`${x}_${y + yOffset + i}_${z}`, blocksCount[BlockType.tree])
matrix.setPosition(x, y + yOffset + i, z)
blocks[BlockType.tree].setMatrixAt(
blocksCount[BlockType.tree]++,
matrix
)
}
// leaf
for (let i = -3; i < 3; i++) {
for (let j = -3; j < 3; j++) {
for (let k = -3; k < 3; k++) {
if (i === 0 && k === 0) {
continue
}
const leafOffset =
noise.get(
(x + i + j) / noise.leafGap,
(z + k) / noise.leafGap,
noise.leafSeed
) * noise.leafAmp
if (leafOffset > noise.leafThreshold) {
idMap.set(
`${x + i}_${y + yOffset + noise.treeHeight + j}_${z + k}`,
blocksCount[BlockType.leaf]
)
matrix.setPosition(
x + i,
y + yOffset + noise.treeHeight + j,
z + k
)
blocks[BlockType.leaf].setMatrixAt(
blocksCount[BlockType.leaf]++,
matrix
)
}
}
}
}
}
}
}
for (const block of customBlocks) {
if (
block.x > -chunkSize * distance + chunkSize * chunk.x &&
block.x < chunkSize * distance + chunkSize + chunkSize * chunk.x &&
block.z > -chunkSize * distance + chunkSize * chunk.y &&
block.z < chunkSize * distance + chunkSize + chunkSize * chunk.y
) {
if (block.placed) {
// placed blocks
matrix.setPosition(block.x, block.y, block.z)
blocks[block.type].setMatrixAt(blocksCount[block.type]++, matrix)
} else {
// removed blocks
const id = idMap.get(`${block.x}_${block.y}_${block.z}`)
blocks[block.type].setMatrixAt(
id!,
new THREE.Matrix4().set(
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
)
)
}
}
}
const arrays = blocks.map(block => block.instanceMatrix.array)
postMessage({ idMap, arrays, blocksCount })
// console.log(performance.now() - p1)
}
import grass from '../../static/block-icon/grass.png'
import stone from '../../static/block-icon/stone.png'
import tree from '../../static/block-icon/tree.png'
import wood from '../../static/block-icon/wood.png'
import diamond from '../../static/block-icon/diamond.png'
import quartz from '../../static/block-icon/quartz.png'
import glass from '../../static/block-icon/glass.png'
import { isMobile } from '../../utils'
export default class Bag {
constructor() {
if (isMobile) return
this.bag.className = 'bag'
this.items[0].classList.add('selected')
for (let i = 0; i < this.items.length; i++) {
this.bag.appendChild(this.items[i])
}
document.body.appendChild(this.bag)
document.body.addEventListener('keydown', (e: KeyboardEvent) => {
if (isNaN(parseInt(e.key)) || e.key === '0') {
return
}
for (let i = 0; i < this.items.length; i++) {
this.items[i].classList.remove('selected')
}
this.current = parseInt(e.key) - 1
this.items[this.current].classList.add('selected')
})
document.body.addEventListener('wheel', (e: WheelEvent) => {
if (!this.wheelGap) {
this.wheelGap = true
setTimeout(() => {
this.wheelGap = false
}, 100)
if (e.deltaY > 0) {
this.current++
this.current > 9 && (this.current = 0)
} else if (e.deltaY < 0) {
this.current--
this.current < 0 && (this.current = 9)
}
for (let i = 0; i < this.items.length; i++) {
this.items[i].classList.remove('selected')
}
this.items[this.current].classList.add('selected')
}
})
}
wheelGap = false
current = 0
icon = [grass, stone, tree, wood, diamond, quartz, glass]
iconIndex = 0
y = 0
bag = document.createElement('div')
items = new Array(10).fill(null).map(() => {
let item = document.createElement('div')
item.className = 'item'
let img = document.createElement('img')
if (this.icon[this.iconIndex]) {
img.className = 'icon'
img.alt = 'block'
img.src = this.icon[this.iconIndex++]
item.appendChild(img)
}
return item
})
}
/**
* Display current FPS
*/
export default class FPS {
constructor() {
this.fps.className = 'fps'
this.fps.innerHTML = `FPS: 60`
document.body.appendChild(this.fps)
}
p1 = performance.now()
p2 = performance.now()
gap = performance.now()
fps = document.createElement('div')
count = 0
update = () => {
this.p1 = performance.now()
this.count++
if (performance.now() - this.gap > 1000) {
this.fps.innerHTML = `FPS: ${this.count}`
this.gap = performance.now()
this.count = 0
}
this.p2 = this.p1
}
}
import FPS from './fps'
import Bag from './bag'
import Terrain from '../terrain'
import Block from '../terrain/mesh/block'
import Control from '../control'
import { Mode } from '../player'
import Joystick from './joystick'
import { isMobile } from '../utils'
import * as THREE from 'three'
export default class UI {
constructor(terrain: Terrain, control: Control) {
this.fps = new FPS()
this.bag = new Bag()
this.joystick = new Joystick(control)
this.crossHair.className = 'cross-hair'
this.crossHair.innerHTML = '+'
document.body.appendChild(this.crossHair)
// play
this.play?.addEventListener('click', () => {
if (this.play?.innerHTML === 'Play') {
this.onPlay()
// reset game
terrain.noise.seed = Math.random()
terrain.noise.stoneSeed = Math.random()
terrain.noise.treeSeed = Math.random()
terrain.noise.coalSeed = Math.random()
terrain.noise.leafSeed = Math.random()
terrain.customBlocks = []
terrain.initBlocks()
terrain.generate()
terrain.camera.position.y = 40
control.player.setMode(Mode.walking)
}
!isMobile && control.control.lock()
})
// save load
this.save?.addEventListener('click', () => {
if (this.save?.innerHTML === 'Save and Exit') {
// save game
window.localStorage.setItem(
'block',
JSON.stringify(terrain.customBlocks)
)
window.localStorage.setItem('seed', JSON.stringify(terrain.noise.seed))
window.localStorage.setItem(
'position',
JSON.stringify({
x: terrain.camera.position.x,
y: terrain.camera.position.y,
z: terrain.camera.position.z
})
)
// ui update
this.onExit()
this.onSave()
} else {
// load game
terrain.noise.seed =
Number(window.localStorage.getItem('seed')) ?? Math.random()
const customBlocks =
(JSON.parse(
window.localStorage.getItem('block') || 'null'
) as Block[]) ?? []
terrain.customBlocks = customBlocks
terrain.initBlocks()
terrain.generate()
const position =
(JSON.parse(window.localStorage.getItem('position') || 'null') as {
x: number
y: number
z: number
}) ?? null
position && (terrain.camera.position.x = position.x)
position && (terrain.camera.position.y = position.y)
position && (terrain.camera.position.z = position.z)
// ui update
this.onPlay()
this.onLoad()
!isMobile && control.control.lock()
}
})
// guide
this.feature?.addEventListener('click', () => {
this.features?.classList.remove('hidden')
})
this.back?.addEventListener('click', () => {
this.features?.classList.add('hidden')
})
// setting
this.setting?.addEventListener('click', () => {
this.settings?.classList.remove('hidden')
})
this.settingBack?.addEventListener('click', () => {
this.settings?.classList.add('hidden')
})
// render distance
this.distanceInput?.addEventListener('input', (e: Event) => {
if (this.distance && e.target instanceof HTMLInputElement) {
this.distance.innerHTML = `Render Distance: ${e.target.value}`
}
})
// fov
this.fovInput?.addEventListener('input', (e: Event) => {
if (this.fov && e.target instanceof HTMLInputElement) {
this.fov.innerHTML = `Field of View: ${e.target.value}`
control.camera.fov = parseInt(e.target.value)
control.camera.updateProjectionMatrix()
}
})
// music
this.musicInput?.addEventListener('input', (e: Event) => {
if (this.fov && e.target instanceof HTMLInputElement) {
const disabled = e.target.value === '0'
control.audio.disabled = disabled
this.music!.innerHTML = `Music: ${disabled ? 'Off' : 'On'}`
}
})
// apply settings
this.settingBack?.addEventListener('click', () => {
if (this.distanceInput instanceof HTMLInputElement) {
terrain.distance = parseInt(this.distanceInput.value)
terrain.maxCount =
(terrain.distance * terrain.chunkSize * 2 + terrain.chunkSize) ** 2 +
500
terrain.initBlocks()
terrain.generate()
terrain.scene.fog = new THREE.Fog(
0x87ceeb,
1,
terrain.distance * 24 + 24
)
}
})
// menu and fullscreen
document.body.addEventListener('keydown', (e: KeyboardEvent) => {
// menu
if (e.key === 'e' && document.pointerLockElement) {
!isMobile && control.control.unlock()
}
// fullscreen
if (e.key === 'f') {
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
document.body.requestFullscreen()
}
}
})
// exit
this.exit?.addEventListener('click', () => {
this.onExit()
})
// play / pause handler
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement) {
this.onPlay()
} else {
this.onPause()
}
})
// disable context menu
document.addEventListener('contextmenu', e => {
e.preventDefault()
})
// fallback lock handler
document.querySelector('canvas')?.addEventListener('click', (e: Event) => {
e.preventDefault()
!isMobile && control.control.lock()
})
}
fps: FPS
bag: Bag
joystick: Joystick
menu = document.querySelector('.menu')
crossHair = document.createElement('div')
// buttons
play = document.querySelector('#play')
control = document.querySelector('#control')
setting = document.querySelector('#setting')
feature = document.querySelector('#feature')
back = document.querySelector('#back')
exit = document.querySelector('#exit')
save = document.querySelector('#save')
// modals
saveModal = document.querySelector('.save-modal')
loadModal = document.querySelector('.load-modal')
settings = document.querySelector('.settings')
features = document.querySelector('.features')
github = document.querySelector('.github')
// settings
distance = document.querySelector('#distance')
distanceInput = document.querySelector('#distance-input')
fov = document.querySelector('#fov')
fovInput = document.querySelector('#fov-input')
music = document.querySelector('#music')
musicInput = document.querySelector('#music-input')
settingBack = document.querySelector('#setting-back')
onPlay = () => {
isMobile && this.joystick.init()
this.menu?.classList.add('hidden')
this.menu?.classList.remove('start')
this.play && (this.play.innerHTML = 'Resume')
this.crossHair.classList.remove('hidden')
this.github && this.github.classList.add('hidden')
this.feature?.classList.add('hidden')
}
onPause = () => {
this.menu?.classList.remove('hidden')
this.crossHair.classList.add('hidden')
this.save && (this.save.innerHTML = 'Save and Exit')
this.github && this.github.classList.remove('hidden')
}
onExit = () => {
this.menu?.classList.add('start')
this.play && (this.play.innerHTML = 'Play')
this.save && (this.save.innerHTML = 'Load Game')
this.feature?.classList.remove('hidden')
}
onSave = () => {
this.saveModal?.classList.remove('hidden')
setTimeout(() => {
this.saveModal?.classList.add('show')
})
setTimeout(() => {
this.saveModal?.classList.remove('show')
}, 1000)
setTimeout(() => {
this.saveModal?.classList.add('hidden')
}, 1350)
}
onLoad = () => {
this.loadModal?.classList.remove('hidden')
setTimeout(() => {
this.loadModal?.classList.add('show')
})
setTimeout(() => {
this.loadModal?.classList.remove('show')
}, 1000)
setTimeout(() => {
this.loadModal?.classList.add('hidden')
}, 1350)
}
update = () => {
this.fps.update()
}
}
import * as THREE from 'three'
import Control from '../../control'
import { Mode } from '../../player'
import { htmlToDom } from '../../utils'
import UI from './joystick.html?raw'
enum ActionKey {
FRONT = 'front',
LEFT = 'left',
RIGHT = 'right',
BACK = 'back',
MODE = 'mode',
JUMP = 'jump',
UP = 'up',
DOWN = 'down'
}
export default class Joystick {
constructor(control: Control) {
this.control = control
this.euler = new THREE.Euler(0, 0, 0, 'YXZ')
}
control: Control
pageX = 0
pageY = 0
clickX = 0
clickY = 0
euler: THREE.Euler
clickTimeout?: ReturnType<typeof setTimeout>
clickInterval?: ReturnType<typeof setInterval>
hold = false
// emit keyboard event
private emitKeyboardEvent = (key: string) => {
return {
key
} as KeyboardEvent
}
// emit click event
private emitClickEvent = (button: number) => {
return {
button,
preventDefault: () => { }
} as MouseEvent
}
// init joystick button
private initButton = ({
actionKey,
key
}: {
actionKey: ActionKey
key: string
}) => {
const button = document.querySelector(
`#action-${actionKey}`
) as HTMLButtonElement
button.addEventListener('pointermove', e => {
e.stopPropagation()
})
button.addEventListener('pointerdown', e => {
this.control.setMovementHandler(this.emitKeyboardEvent(key))
e.stopPropagation()
})
button.addEventListener('pointerup', e => {
this.control.resetMovementHandler(this.emitKeyboardEvent(key))
e.stopPropagation()
})
// extra config for mode switch button
if (actionKey === ActionKey.MODE && key === 'q') {
this.initButton({ actionKey: ActionKey.MODE, key: ' ' })
button.addEventListener('pointerdown', () => {
if (this.control.player.mode === Mode.flying) {
document.querySelector('#action-down')?.classList.remove('hidden')
} else {
document.querySelector('#action-down')?.classList.add('hidden')
}
})
}
}
init = () => {
htmlToDom(UI)
this.initButton({ actionKey: ActionKey.FRONT, key: 'w' })
this.initButton({ actionKey: ActionKey.LEFT, key: 'a' })
this.initButton({ actionKey: ActionKey.RIGHT, key: 'd' })
this.initButton({ actionKey: ActionKey.BACK, key: 's' })
this.initButton({ actionKey: ActionKey.MODE, key: 'q' })
this.initButton({ actionKey: ActionKey.UP, key: ' ' })
this.initButton({ actionKey: ActionKey.DOWN, key: 'Shift' })
// camera control
document.addEventListener('pointermove', e => {
if (this.pageX !== 0 || this.pageY !== 0) {
this.euler.setFromQuaternion(this.control.camera.quaternion)
this.euler.y -= 0.01 * (e.pageX - this.pageX)
this.euler.x -= 0.01 * (e.pageY - this.pageY)
this.euler.x = Math.max(
-Math.PI / 2,
Math.min(Math.PI / 2, this.euler.x)
)
this.control.camera.quaternion.setFromEuler(this.euler)
}
this.pageX = e.pageX
this.pageY = e.pageY
this.clickTimeout && clearTimeout(this.clickTimeout)
})
// click control
document.addEventListener('pointerdown', e => {
this.clickX = e.pageX
this.clickY = e.pageY
this.clickTimeout = setTimeout(() => {
if (e.pageX === this.clickX && e.pageY === this.clickY) {
this.control.mousedownHandler(this.emitClickEvent(0))
this.clickInterval = setInterval(() => {
this.control.mousedownHandler(this.emitClickEvent(0))
}, 333)
this.hold = true
}
}, 500)
})
document.addEventListener('pointerup', e => {
this.clickTimeout && clearTimeout(this.clickTimeout)
this.clickInterval && clearInterval(this.clickInterval)
if (!this.hold && e.pageX === this.clickX && e.pageY === this.clickY) {
this.control.mousedownHandler(this.emitClickEvent(2))
}
this.hold = false
this.pageX = 0
this.pageY = 0
})
}
}
<div class="joystick">
<button id="action-front" class="joystick-button front" role="div">🔼</button>
<button id="action-left" class="joystick-button left" role="div">◀️</button>
<button id="action-right" class="joystick-button right" role="div">▶️</button>
<button id="action-back" class="joystick-button back" role="div">🔽</button>
<button id="action-mode" class="joystick-button mode" role="div">⏹️</button>
<button id="action-up" class="joystick-button up" role="div">🔼</button>
<button id="action-down" class="joystick-button down hidden" role="div">
🔽
</button>
</div>
<style>
.joystick {
position: fixed;
bottom: 0;
display: grid;
width: 100%;
grid-template-columns: repeat(auto-fill, 50px);
grid-template-rows: repeat(3, 1fr);
gap: 10px;
padding: 10px;
}
.joystick-button {
width: 52px;
height: 50px;
font-size: 50px;
line-height: 50px;
padding: 0px;
border: none;
background-color: transparent;
}
.front {
grid-column: 2 / 3;
grid-row: 1;
}
.left {
grid-column: 1 / 2;
grid-row: 2;
}
.right {
grid-column: 3 / 4;
grid-row: 2;
}
.back {
grid-column: 2 / 3;
grid-row: 3;
}
.mode {
grid-column: 2 / 3;
grid-row: 2;
}
.up {
grid-column: -3 / -2;
grid-row: 2;
}
.down {
grid-column: -3 / -2;
grid-row: 3;
}
</style>
export const htmlToDom = (html: string) => {
const templateDom = document.createElement('template')
templateDom.innerHTML = html
window.document.body.appendChild(templateDom.content)
}
export const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
navigator.userAgent
)
/// <reference types="vite/client" />
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "ESNext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": [
"./node_modules/@types",
], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": [
"node_modules",
".build"
]
}
\ No newline at end of file
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true
},
"include": ["./src"]
}
export default {
build: {
chunkSizeWarningLimit: 2000,
assetsInlineLimit: 0,
sourcemap: true
}
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@types/three@^0.137.0":
version "0.137.0"
resolved "https://registry.yarnpkg.com/@types/three/-/three-0.137.0.tgz#6047e0658262b4de7c464a40288f9071ddd9a6d5"
integrity sha512-Xc5EAlfYmgrCLI/VlSVqiRJAtzhWF0Rw2jSq48nqJy+Hcb5sfDOXyfZn1+RNcHyi9l8CeCAXCZygO8IyeOJVEA==
esbuild-android-arm64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.21.tgz#8842d0c3b7c81fbe2dc46ddb416ffd6eb822184b"
integrity sha512-Bqgld1TY0wZv8TqiQmVxQFgYzz8ZmyzT7clXBDZFkOOdRybzsnj8AZuK1pwcLVA7Ya6XncHgJqIao7NFd3s0RQ==
esbuild-darwin-64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.21.tgz#ec7df02ad88ecf7f8fc23a3ed7917e07dea0c9c9"
integrity sha512-j+Eg+e13djzyYINVvAbOo2/zvZ2DivuJJTaBrJnJHSD7kUNuGHRkHoSfFjbI80KHkn091w350wdmXDNSgRjfYQ==
esbuild-darwin-arm64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.21.tgz#0c2a977edec1ef54097ee56a911518c820d4e5e4"
integrity sha512-nDNTKWDPI0RuoPj5BhcSB2z5EmZJJAyRtZLIjyXSqSpAyoB8eyAKXl4lB8U2P78Fnh4Lh1le/fmpewXE04JhBQ==
esbuild-freebsd-64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.21.tgz#f5b5fc1d031286c3a0949d1bda7db774b7d0404e"
integrity sha512-zIurkCHXhxELiDZtLGiexi8t8onQc2LtuE+S7457H/pP0g0MLRKMrsn/IN4LDkNe6lvBjuoZZi2OfelOHn831g==
esbuild-freebsd-arm64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.21.tgz#a05cab908013e4992b31a675850b8c44eb468c0c"
integrity sha512-wdxMmkJfbwcN+q85MpeUEamVZ40FNsBa9mPq8tAszDn8TRT2HoJvVRADPIIBa9SWWwlDChIMjkDKAnS3KS/sPA==
esbuild-linux-32@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.21.tgz#638d244cc58b951f447addb4bade628d126ef84b"
integrity sha512-fmxvyzOPPh2xiEHojpCeIQP6pXcoKsWbz3ryDDIKLOsk4xp3GbpHIEAWP0xTeuhEbendmvBDVKbAVv3PnODXLg==
esbuild-linux-64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.21.tgz#8eb634abee928be7e35b985fafbfef2f2e31397f"
integrity sha512-edZyNOv1ql+kpmlzdqzzDjRQYls+tSyi4QFi+PdBhATJFUqHsnNELWA9vMSzAaInPOEaVUTA5Ml28XFChcy4DA==
esbuild-linux-arm64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.21.tgz#e05599ea6253b58394157da162d856f3ead62f9e"
integrity sha512-t5qxRkq4zdQC0zXpzSB2bTtfLgOvR0C6BXYaRE/6/k8/4SrkZcTZBeNu+xGvwCU4b5dU9ST9pwIWkK6T1grS8g==
esbuild-linux-arm@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.21.tgz#1ae1078231cf689d3ba894a32d3723c0be9b91fd"
integrity sha512-aSU5pUueK6afqmLQsbU+QcFBT62L+4G9hHMJDHWfxgid6hzhSmfRH9U/f+ymvxsSTr/HFRU4y7ox8ZyhlVl98w==
esbuild-linux-mips64le@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.21.tgz#f05be62d126764e99b37edcac5bb49b78c7a8890"
integrity sha512-jLZLQGCNlUsmIHtGqNvBs3zN+7a4D9ckf0JZ+jQTwHdZJ1SgV9mAjbB980OFo66LoY+WeM7t3WEnq3FjI1zw4A==
esbuild-linux-ppc64le@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.21.tgz#592c98d82dad7982268ef8deed858c4566f07ab1"
integrity sha512-4TWxpK391en2UBUw6GSrukToTDu6lL9vkm3Ll40HrI08WG3qcnJu7bl8e1+GzelDsiw1QmfAY/nNvJ6iaHRpCQ==
esbuild-linux-riscv64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.21.tgz#0db7bd6f10d8f9afea973a7d6bf87b449b864b7b"
integrity sha512-fElngqOaOfTsF+u+oetDLHsPG74vB2ZaGZUqmGefAJn3a5z9Z2pNa4WpVbbKgHpaAAy5tWM1m1sbGohj6Ki6+Q==
esbuild-linux-s390x@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.21.tgz#254a9354d34c9d1b41a3e21d2ec9269cbbb2c5df"
integrity sha512-brleZ6R5fYv0qQ7ZBwenQmP6i9TdvJCB092c/3D3pTLQHBGHJb5zWgKxOeS7bdHzmLy6a6W7GbFk6QKpjyD6QA==
esbuild-netbsd-64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.21.tgz#4cb783d060b02bf3b897a9a12cce2b3b547726f8"
integrity sha512-nCEgsLCQ8RoFWVV8pVI+kX66ICwbPP/M9vEa0NJGIEB/Vs5sVGMqkf67oln90XNSkbc0bPBDuo4G6FxlF7PN8g==
esbuild-openbsd-64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.21.tgz#f886b93feefddbe573528fa4b421c9c6e2bc969b"
integrity sha512-h9zLMyVD0T73MDTVYIb/qUTokwI6EJH9O6wESuTNq6+XpMSr6C5aYZ4fvFKdNELW+Xsod+yDS2hV2JTUAbFrLA==
esbuild-sunos-64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.21.tgz#3829e4d57d4cb6950837fe90b0b67cdfb37cf13a"
integrity sha512-Kl+7Cot32qd9oqpLdB1tEGXEkjBlijrIxMJ0+vlDFaqsODutif25on0IZlFxEBtL2Gosd4p5WCV1U7UskNQfXA==
esbuild-windows-32@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.21.tgz#b858a22d1a82e53cdc59310cd56294133f7a95e7"
integrity sha512-V7vnTq67xPBUCk/9UtlolmQ798Ecjdr1ZoI1vcSgw7M82aSSt0eZdP6bh5KAFZU8pxDcx3qoHyWQfHYr11f22A==
esbuild-windows-64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.21.tgz#7bb5a027d5720cf9caf18a4bedd11327208f1f12"
integrity sha512-kDgHjKOHwjfJDCyRGELzVxiP/RBJBTA+wyspf78MTTJQkyPuxH2vChReNdWc+dU2S4gIZFHMdP1Qrl/k22ZmaA==
esbuild-windows-arm64@0.14.21:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.21.tgz#25df54521ad602c826b262ea2e7cc1fe80f5c2f5"
integrity sha512-8Sbo0zpzgwWrwjQYLmHF78f7E2xg5Ve63bjB2ng3V2aManilnnTGaliq2snYg+NOX60+hEvJHRdVnuIAHW0lVw==
esbuild@^0.14.14:
version "0.14.21"
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.21.tgz#b3e05f900f1c4394f596d60d63d9816468f0f671"
integrity sha512-7WEoNMBJdLN993dr9h0CpFHPRc3yFZD+EAVY9lg6syJJ12gc5fHq8d75QRExuhnMkT2DaRiIKFThRvDWP+fO+A==
optionalDependencies:
esbuild-android-arm64 "0.14.21"
esbuild-darwin-64 "0.14.21"
esbuild-darwin-arm64 "0.14.21"
esbuild-freebsd-64 "0.14.21"
esbuild-freebsd-arm64 "0.14.21"
esbuild-linux-32 "0.14.21"
esbuild-linux-64 "0.14.21"
esbuild-linux-arm "0.14.21"
esbuild-linux-arm64 "0.14.21"
esbuild-linux-mips64le "0.14.21"
esbuild-linux-ppc64le "0.14.21"
esbuild-linux-riscv64 "0.14.21"
esbuild-linux-s390x "0.14.21"
esbuild-netbsd-64 "0.14.21"
esbuild-openbsd-64 "0.14.21"
esbuild-sunos-64 "0.14.21"
esbuild-windows-32 "0.14.21"
esbuild-windows-64 "0.14.21"
esbuild-windows-arm64 "0.14.21"
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
guess@^1.0.2:
version "1.0.2"
resolved "http://mirrors.csdn.net.cn/repository/csdn-npm-mirrors/guess/-/guess-1.0.2.tgz#1297e6be4b51855c958bbf8f2a6e23e2234ce44b"
integrity sha512-giSVed+pcC74rOB6QutqV44aAY2cZcuwJLpfLbNQdbb6+l+QWktBawYrSYxZozZq4pvFo+RrJuXMA4kblgcH8A==
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
is-core-module@^2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
dependencies:
has "^1.0.3"
nanoid@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
postcss@^8.4.5:
version "8.4.6"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1"
integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA==
dependencies:
nanoid "^3.2.0"
picocolors "^1.0.0"
source-map-js "^1.0.2"
resolve@^1.22.0:
version "1.22.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==
dependencies:
is-core-module "^2.8.1"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
rollup@^2.59.0:
version "2.67.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.67.1.tgz#4402665706fa00f321d446ce45f880e02cf54f01"
integrity sha512-1Sbcs4OuW+aD+hhqpIRl+RqooIpF6uQcfzU/QSI7vGkwADY6cM4iLsBGRM2CGLXDTDN5y/yShohFmnKegSPWzg==
optionalDependencies:
fsevents "~2.3.2"
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
three@^0.137.0:
version "0.137.5"
resolved "https://registry.yarnpkg.com/three/-/three-0.137.5.tgz#a1e34bedd0412f2d8797112973dfadac78022ce6"
integrity sha512-rTyr+HDFxjnN8+N/guZjDgfVxgHptZQpf6xfL/Mo7a5JYIFwK6tAq3bzxYYB4Ae0RosDZlDuP+X5aXDXz+XnHQ==
typescript@^4.5.5:
version "4.5.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
vite@^2.8.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.0.tgz#0646ab9eee805fb24b667889644ac04bc516d0d3"
integrity sha512-ed5rjyeysttuPJX/aKSA0gTB/8ZKLM5xF6FtEuKy1B9DiQbDNFMVMQxnb9JesgBPUMMIJxC8w5KZ/KNWLKFXoA==
dependencies:
esbuild "^0.14.14"
postcss "^8.4.5"
resolve "^1.22.0"
rollup "^2.59.0"
optionalDependencies:
fsevents "~2.3.2"
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册