pages235/index.js
Vitaly 9227ccc7d6 first real cypress test, try on ci
remove refs to unimplemented components for now
2023-08-17 05:35:29 +03:00

631 lines
18 KiB
JavaScript

//@ts-check
/* global THREE */
require('./lib/chat')
require('./lib/menus/components/button')
require('./lib/menus/components/edit_box')
require('./lib/menus/components/slider')
require('./lib/menus/components/hotbar')
require('./lib/menus/components/health_bar')
require('./lib/menus/components/food_bar')
require('./lib/menus/components/breath_bar')
require('./lib/menus/components/debug_overlay')
require('./lib/menus/components/playerlist_overlay')
require('./lib/menus/components/bossbars_overlay')
require('./lib/menus/hud')
require('./lib/menus/play_screen')
require('./lib/menus/pause_screen')
require('./lib/menus/loading_or_error_screen')
require('./lib/menus/keybinds_screen')
require('./lib/menus/options_screen')
require('./lib/menus/advanced_options_screen')
require('./lib/menus/notification')
require('./lib/menus/title_screen')
const { promisify } = require('util')
const browserfs = require('browserfs')
browserfs.install(window)
browserfs.configure({
// todo change to localstorage: mkdir doesnt work for some reason
fs: 'MountableFileSystem',
options: {
"/world": { fs: "LocalStorage" }
},
}, (e) => {
if (e) throw e
})
const _fs = require('fs')
//@ts-ignore
_fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat'].map(key => [key, promisify(_fs[key])])), {
get (target, p, receiver) {
//@ts-ignore
if (!target[p]) throw new Error(`Not implemented fs.promises.${p}`)
return Reflect.get(target, p, receiver)
}
})
//@ts-ignore
_fs.promises.open = async (...args) => {
const fd = await promisify(_fs.open)(...args)
return Object.fromEntries(['read', 'write', 'close'].map(x => [x, async (...args) => {
return await new Promise(resolve => {
_fs[x](fd, ...args, (err, bytesRead, buffer) => {
if (err) throw err
resolve({ buffer, bytesRead })
})
})
}]))
}
const net = require('net')
const Stats = require('stats.js')
// workaround for mineflayer
process.versions.node = '14.0.0'
const mineflayer = require('mineflayer')
const { WorldView, Viewer, MapControls } = require('prismarine-viewer/viewer')
const PrismarineWorld = require('prismarine-world')
const nbt = require('prismarine-nbt')
const pathfinder = require('mineflayer-pathfinder')
const { Vec3 } = require('vec3')
const Cursor = require('./lib/cursor')
//@ts-ignore
global.THREE = require('three')
const { initVR } = require('./lib/vr')
const { activeModalStack, showModal, hideModal, hideCurrentModal, activeModalStacks, replaceActiveModalStack } = require('./lib/globalState')
const { pointerLock, goFullscreen, toNumber } = require('./lib/utils')
const { notification } = require('./lib/menus/notification')
const { removePanorama, addPanoramaCubeMap, initPanoramaOptions } = require('./lib/panorama')
const { createClient } = require('minecraft-protocol')
const { serverOptions, startLocalServer } = require('./lib/createLocalServer')
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration)
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError)
})
})
}
// ACTUAL CODE
let stats
let stats2
stats = new Stats()
stats2 = new Stats()
stats2.showPanel(2)
document.body.appendChild(stats.dom)
stats2.dom.style.left = '80px'
document.body.appendChild(stats2.dom)
window.hideStats = () => {
stats.dom.style.display = 'none'
stats2.dom.style.display = 'none'
}
if (localStorage.hideStats) window.hideStats()
// const debugPitch = document.createElement('span')
// debugPitch.style.cssText = `
// position: absolute;
// top: 0;
// right: 0;
// z-index: 100;
// color:white;
// `
// document.body.appendChild(debugPitch)
const maxPitch = 0.5 * Math.PI
const minPitch = -0.5 * Math.PI
// Create three.js context, add to page
const renderer = new THREE.WebGLRenderer()
renderer.setPixelRatio(window.devicePixelRatio || 1)
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// Create viewer
const viewer = new Viewer(renderer)
initPanoramaOptions(viewer)
const frameLimit = toNumber(localStorage.frameLimit)
let interval = frameLimit && 1000 / frameLimit
window.addEventListener('option-change', (/** @type {any} */{ detail }) => {
if (detail.name === 'frameLimit') interval = toNumber(detail.value) && 1000 / toNumber(detail.value)
})
let postRenderFrameFn = () => { }
let delta = 0
let lastTime = performance.now()
const renderFrame = (/** @type {DOMHighResTimeStamp} */ time) => {
if (window.stopLoop) return
window.requestAnimationFrame(renderFrame)
if (window.stopRender) return
if (interval) {
delta += time - lastTime
lastTime = time
if (delta > interval) {
delta = delta % interval
// continue rendering
} else {
return
}
}
stats?.begin()
stats2?.begin()
viewer.update()
renderer.render(viewer.scene, viewer.camera)
postRenderFrameFn()
stats?.end()
stats2?.end()
}
renderFrame(performance.now())
window.addEventListener('resize', () => {
viewer.camera.aspect = window.innerWidth / window.innerHeight
viewer.camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
})
const loadingScreen = document.getElementById('loading-error-screen')
const hud = document.getElementById('hud')
const optionsScrn = document.getElementById('options-screen')
const keyBindScrn = document.getElementById('keybinds-screen')
const pauseMenu = document.getElementById('pause-screen')
function setLoadingScreenStatus (status, isError = false) {
showModal(loadingScreen)
if (loadingScreen.hasError) return
loadingScreen.hasError = isError
loadingScreen.status = status
}
let mouseMovePostHandle = (e) => { }
let lastMouseCall
function onMouseMove (e) {
if (e.type !== 'touchmove' && !pointerLock.hasPointerLock) return
e.stopPropagation?.()
const now = performance.now()
// todo: limit camera movement for now to avoid unexpected jumps
if (now - lastMouseCall < 4) return
lastMouseCall = now
let { mouseSensX, mouseSensY } = optionsScrn
if (mouseSensY === true) mouseSensY = mouseSensX
// debugPitch.innerText = +debugPitch.innerText + e.movementX
mouseMovePostHandle({
x: e.movementX * mouseSensX * 0.0001,
y: e.movementY * mouseSensY * 0.0001
})
}
window.addEventListener('mousemove', onMouseMove, { capture: true })
function hideCurrentScreens () {
activeModalStacks['main-menu'] = activeModalStack
replaceActiveModalStack('', [])
}
async function main () {
const menu = document.getElementById('play-screen')
menu.addEventListener('connect', e => {
const options = e.detail
menu.style = 'display: none;'
removePanorama()
connect(options)
})
document.querySelector('#title-screen').addEventListener('singleplayer', (e) => {
menu.style = 'display: none;'
removePanorama()
// todo clean
connect({ server: '', port: '', proxy: '', singleplayer: true, username: 'wanderer', password: '' })
})
}
let listeners = []
let timeouts = []
let intervals = []
// only for dom listeners (no removeAllListeners)
// todo refactor them out of connect fn instead
/** @type {import('./lib/utilsTs').RegisterListener} */
const registerListener = (target, event, callback) => {
target.addEventListener(event, callback)
listeners.push({ target, event, callback })
}
const removeAllListeners = () => {
listeners.forEach(({ target, event, callback }) => {
target.removeEventListener(event, callback)
})
listeners = []
}
/**
* @param {{ server: any; port?: string; singleplayer: any; username: any; password: any; proxy: any; botVersion?: any; }} options
*/
async function connect (options) {
const singeplayer = options.singleplayer
loadingScreen.maybeRecoverable = true
const oldSetInterval = window.setInterval
// @ts-ignore
window.setInterval = (callback, ms) => {
const id = oldSetInterval.call(window, callback, ms)
timeouts.push(id)
return id
}
const oldSetTimeout = window.setTimeout
//@ts-ignore
window.setTimeout = (callback, ms) => {
const id = oldSetTimeout.call(window, callback, ms)
timeouts.push(id)
return id
}
const debugMenu = hud.shadowRoot.querySelector('#debug-overlay')
const viewDistance = optionsScrn.renderDistance
const hostprompt = options.server
const proxyprompt = options.proxy
const username = options.username
const password = options.password
let host, port, proxy, proxyport
if (!hostprompt.includes(':')) {
host = hostprompt
port = 25565
} else {
[host, port] = hostprompt.split(':')
port = parseInt(port, 10)
}
if (!proxyprompt.includes(':')) {
proxy = proxyprompt
proxyport = undefined
} else {
[proxy, proxyport] = proxyprompt.split(':')
proxyport = parseInt(proxyport, 10)
}
console.log(`connecting to ${host} ${port} with ${username}`)
if (proxy) {
console.log(`using proxy ${proxy} ${proxyport}`)
//@ts-ignore
net.setProxy({ hostname: proxy, port: proxyport })
}
setLoadingScreenStatus('Logging in')
/** @type {mineflayer.Bot} */
let bot
const destroy = () => {
// simple variant, still buggy
postRenderFrameFn = () => { }
if (bot) {
bot.removeAllListeners()
bot._client.removeAllListeners()
bot._client = undefined
bot = undefined
}
removeAllListeners()
for (const timeout of timeouts) {
clearTimeout(timeout)
}
timeouts = []
for (const interval of intervals) {
clearInterval(interval)
}
intervals = []
}
const handleError = (err) => {
console.log('Encountered error!', err)
// #region rejoin key
const controller = new AbortController()
window.addEventListener('keydown', (e) => {
if (e.code !== 'KeyR') return
controller.abort()
connect(options)
loadingScreen.hasError = false
}, { signal: controller.signal })
// #endregion
setLoadingScreenStatus(`Error encountered. Error message: ${err}`, true)
destroy()
}
const errorAbortController = new AbortController()
window.addEventListener('unhandledrejection', (e) => {
handleError(e.reason)
}, {
signal: errorAbortController.signal
})
try {
if (singeplayer) {
window.worldLoaded = false
//@ts-ignore TODO
Object.assign(serverOptions, _.defaultsDeep(JSON.parse(localStorage.localServerOptions || '{}'), serverOptions))
const server = startLocalServer()
// init world, todo: do it for any async plugins
if (!server.worldsReady) {
await new Promise(resolve => {
server.once('worldsReady', resolve)
})
}
}
bot = mineflayer.createBot({
host,
port,
version: options.botVersion === '' ? false : options.botVersion,
...singeplayer ? {
customPackets: true,
version: serverOptions.version,
connect () { },
} : {},
username,
password,
viewDistance: 'tiny',
checkTimeoutInterval: 240 * 1000,
noPongTimeout: 240 * 1000,
closeTimeout: 240 * 1000
})
if (singeplayer) {
bot.emit('inject_allowed')
bot._client.emit('connect')
// todo need just to call quit if started
loadingScreen.maybeRecoverable = false
}
} catch (err) {
handleError(err)
}
if (!bot) return
hud.preload(bot)
// bot.on('inject_allowed', () => {
// loadingScreen.maybeRecoverable = false
// })
bot.on('error', handleError)
bot.on('kicked', (kickReason) => {
console.log('User was kicked!', kickReason)
setLoadingScreenStatus(`The Minecraft server kicked you. Kick reason: ${kickReason}`, true)
destroy()
})
bot.on('end', (endReason) => {
console.log('disconnected for', endReason)
destroy()
setLoadingScreenStatus(`You have been disconnected from the server. End reason: ${endReason}`, true)
})
bot.once('login', () => {
// server is ok, add it to the history
/** @type {string[]} */
const serverHistory = JSON.parse(localStorage.getItem('serverHistory') || '[]')
serverHistory.unshift(options.server)
localStorage.setItem('serverHistory', JSON.stringify([...new Set(serverHistory)]))
setLoadingScreenStatus('Loading world')
})
bot.once('spawn', () => {
// todo display notification if not critical
errorAbortController.abort()
const mcData = require('minecraft-data')(bot.version)
setLoadingScreenStatus('Placing blocks (starting viewer)')
console.log('bot spawned - starting viewer')
const version = bot.version
const center = bot.entity.position
const worldView = new WorldView(bot.world, viewDistance, center)
optionsScrn.isInsideWorld = true
optionsScrn.addEventListener('fov_changed', (e) => {
viewer.camera.fov = e.detail.fov
viewer.camera.updateProjectionMatrix()
})
viewer.setVersion(version)
window.worldView = worldView
window.bot = bot
window.mcData = mcData
window.viewer = viewer
window.Vec3 = Vec3
window.pathfinder = pathfinder
window.debugMenu = debugMenu
window.settings = optionsScrn
window.renderer = renderer
initVR(bot, renderer, viewer)
const cursor = new Cursor(viewer, renderer, bot)
postRenderFrameFn = () => {
debugMenu.cursorBlock = cursor.cursorBlock
viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
cursor.update(bot)
}
try {
const gl = renderer.getContext()
debugMenu.rendererDevice = gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL)
} catch (err) {
console.error(err)
debugMenu.rendererDevice = '???'
}
// Link WorldView and Viewer
viewer.listen(worldView)
worldView.listenToBot(bot)
worldView.init(bot.entity.position)
// Bot position callback
function botPosition () {
// this might cause lag, but not sure
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
worldView.updatePosition(bot.entity.position)
}
bot.on('move', botPosition)
botPosition()
setLoadingScreenStatus('Setting callbacks')
mouseMovePostHandle = ({ x, y }) => {
bot.entity.pitch -= y
bot.entity.pitch = Math.max(minPitch, Math.min(maxPitch, bot.entity.pitch))
bot.entity.yaw -= x
}
function changeCallback () {
notification.show = false
if (!pointerLock.hasPointerLock && activeModalStack.length === 0) {
showModal(pauseMenu)
}
}
registerListener(document, 'pointerlockchange', changeCallback, false)
let lastTouch
registerListener(document, 'touchmove', (e) => {
window.scrollTo(0, 0)
e.preventDefault()
e.stopPropagation()
if (lastTouch !== undefined) {
onMouseMove({ movementX: e.touches[0].pageX - lastTouch.pageX, movementY: e.touches[0].pageY - lastTouch.pageY, type: 'touchmove' })
}
lastTouch = e.touches[0]
}, { passive: false })
registerListener(document, 'touchend', (e) => {
lastTouch = undefined
}, { passive: false })
registerListener(document, 'contextmenu', (e) => e.preventDefault(), false)
registerListener(document, 'blur', (e) => {
bot.clearControlStates()
}, false)
registerListener(document, 'keydown', (e) => {
if (activeModalStack.length) return
keyBindScrn.keymaps.forEach(km => {
if (e.code === km.key) {
switch (km.defaultKey) {
case 'KeyQ':
if (bot.heldItem) bot.tossStack(bot.heldItem)
break
case 'ControlLeft':
bot.setControlState('sprint', true)
break
case 'ShiftLeft':
bot.setControlState('sneak', true)
break
case 'Space':
bot.setControlState('jump', true)
break
case 'KeyD':
bot.setControlState('right', true)
break
case 'KeyA':
bot.setControlState('left', true)
break
case 'KeyS':
bot.setControlState('back', true)
break
case 'KeyW':
bot.setControlState('forward', true)
break
}
}
})
}, false)
registerListener(document, 'keyup', (e) => {
keyBindScrn.keymaps.forEach(km => {
if (e.code === km.key) {
switch (km.defaultKey) {
case 'ControlLeft':
bot.setControlState('sprint', false)
break
case 'ShiftLeft':
bot.setControlState('sneak', false)
break
case 'Space':
bot.setControlState('jump', false)
break
case 'KeyD':
bot.setControlState('right', false)
break
case 'KeyA':
bot.setControlState('left', false)
break
case 'KeyS':
bot.setControlState('back', false)
break
case 'KeyW':
bot.setControlState('forward', false)
break
}
}
})
}, false)
setLoadingScreenStatus('Done!')
console.log('Done!')
hud.init(renderer, bot, host)
hud.style.display = 'block'
setTimeout(function () {
if (loadingScreen.hasError) return
// remove loading screen, wait a second to make sure a frame has properly rendered
hideCurrentScreens()
}, singeplayer ? 0 : 2500)
})
}
window.addEventListener('mousedown', (e) => {
pointerLock.requestPointerLock()
})
window.addEventListener('keydown', (e) => {
if (e.code !== 'Escape') return
if (activeModalStack.length) {
hideCurrentModal(undefined, () => {
if (!activeModalStack.length) {
pointerLock.justHitEscape = true
}
})
} else {
if (pointerLock.hasPointerLock) {
document.exitPointerLock()
} else {
document.dispatchEvent(new Event('pointerlockchange'))
}
}
})
window.addEventListener('keydown', (e) => {
if (e.code === 'F11') {
e.preventDefault()
goFullscreen(true)
}
if (e.code === 'KeyL') {
console.clear()
}
// if (e.code === 'KeyD') {
// debugPitch.innerText = '0'
// }
})
addPanoramaCubeMap()
showModal(document.getElementById('title-screen'))
main()