UI reorganized and options (#257)

* added readme pt-pt

* ui reorganized, keybinds, foodbar, panorama, etc
This commit is contained in:
KalmeMarq 2021-12-24 12:00:07 +00:00 committed by GitHub
parent 1033404f91
commit ed011b07fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2638 additions and 1519 deletions

View file

@ -4,8 +4,8 @@
[![Discord](https://img.shields.io/badge/chat-on%20discord-brightgreen.svg)](https://discord.gg/GsEFRM8)
[![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/PrismarineJS/prismarine-web-client)
| 🇺🇸 [English](README.md) | 🇷🇺 [Russian](README_RU.md) |
| ----------------------- | -------------------------- |
| 🇺🇸 [English](README.md) | 🇷🇺 [Russian](README_RU.md) | 🇵🇹 [Portuguese](README_PT.md) |
| ----------------------- | -------------------------- | ---------------------------- |
A Minecraft client running in a web page. **Live demo at https://webclient.prismarine.js.org/**

104
README_PT.md Normal file
View file

@ -0,0 +1,104 @@
# prismarine-web-client
[![NPM version](https://img.shields.io/npm/v/prismarine-web-client.svg)](http://npmjs.com/package/prismarine-web-client)
[![Build Status](https://github.com/PrismarineJS/prismarine-web-client/workflows/CI/badge.svg)](https://github.com/PrismarineJS/prismarine-web-client/actions?query=workflow%3A%22CI%22)
[![Discord](https://img.shields.io/badge/chat-on%20discord-brightgreen.svg)](https://discord.gg/GsEFRM8)
[![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/PrismarineJS/prismarine-web-client)
| 🇺🇸 [English](README.md) | 🇷🇺 [Russian](README_RU.md) | 🇵🇹 [Portuguese](README_PT.md) |
| ----------------------- | -------------------------- | ---------------------------- |
Um cliente de Minecraft a funcionar numa página web. **Demostração em https://webclient.prismarine.js.org/**
## Como functiona
prismarine-web-client executa mineflayer e prismarine-viewer no teu navegador, que se conecta por WebSocket a uma proxy
que traduz o conexão do WebSocket em TCP para poderes conectar-te a servidores normais do Minecraft. Prismarine-web-client é basiado em:
* [prismarine-viewer](https://github.com/PrismarineJS/prismarine-viewer) para renderizar o mundo
* [mineflayer](https://github.com/PrismarineJS/mineflayer) um API incrível do cliente de Minecraft
Da uma olhada nestes módulos se quiseres entender mais sobre como isto funciona e poderes contribuir!
## Captura de tela
![Captura de tela do prismarine-web-client em ação](screenshot.png)
## Demostração ao vivo
Clica neste endereço para o abrires no navegador, sem instalação necessária: https://webclient.prismarine.js.org/
*Testado no Chrome & Firefox para plataformas desktop.*
## Uso
Para hospedá-lo por si próprio, execute estes comandos no bash:
```bash
$ npm install -g prismarine-web-client
$ prismarine-web-client
```
Finalmente, abra `http://localhost:8080` no seu navigador.
## Conteúdos
* Mostra criaturas e os jogadores
* Mostra os blocos
* Movimento (podes mover-te, e também ver entidades a mover-se em tempo real)
* Colocar e destruir blocos
## Planeamentos
* Containers (inventário, baús, etc.)
* Sons
* Mais interações no mundo (atacar entidades, etc.)
* Renderizar cosméticos (ciclo dia-noite, nevoeiro, etc.)
## Desenvolvimentos
Se estiveres a contribuir/fazer alterações, precisas intalá-lo de outra forma.
Primeiro, clona o repositório.
Depois, defina o seu diretório de trabalho para o do repositório. Por examplo:
```bash
$ cd ~/prismarine-web-client/
```
Finalmente, execute
```bash
$ npm install
$ npm start
```
Isto vai começar o express e webpack no modo de desenvolvimento; quando salvares um arquivo, o build vai ser executado de novo (demora 5s),
e podes atualizar a página para veres o novo resultado.
Conecta em http://localhost:8080 no teu navegador.
Poderás ter que desativar o auto salvar no teu IDE para evitar estar constantemente a reconstruir; see https://webpack.js.org/guides/development/#adjusting-your-text-editor.
Para conferir a build de produção (vai demorar alguns minutos para terminar), podes executar `npm run build-start`.
Se estiveres interessado em contribuir, podes dar uma vista de olhos nos projetos em https://github.com/PrismarineJS/prismarine-web-client/projects.
Algumas variáveis estão expostas no objeto global ``window`` para depuração:
* ``bot``
* ``viewer``
* ``mcData``
* ``worldView``
* ``Vec3``
* ``pathfinder``
* ``debugMenu``
### Adicionar coisas no debugMenu
debugMenu.customEntries['myKey'] = 'myValue'
delete debugMenu.customEntries['myKey']
### Alguns exemplos de depuração
Na devtools do chrome:
* `bot.chat('test')` permite usar o chat
* `bot.chat(JSON.stringify(Object.values(bot.players).map(({username, ping}) => ({username, ping}))))` display the ping of everyone
* `window.bot.entity.position.y += 5` saltar
* `bot.chat(JSON.stringify(bot.findBlock({matching:(block) => block.name==='diamond_ore', maxDistance:256}).position))` encontrar a posição de um bloco de diamante
* `bot.physics.stepHeight = 2` permite andar sobre os blocos
* `bot.physics.sprintSpeed = 5` andar mais rápido
* `bot.loadPlugin(pathfinder.pathfinder)` em seguida `bot.pathfinder.goto(new pathfinder.goals.GoalXZ(100, 100))` para ir para a posição 100, 100
Para mais ideas de depuração, leia o documento [mineflayer](https://github.com/PrismarineJS/mineflayer).

View file

@ -4,8 +4,8 @@
[![Discord](https://img.shields.io/badge/chat-on%20discord-brightgreen.svg)](https://discord.gg/GsEFRM8)
[![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/PrismarineJS/prismarine-web-client)
| 🇺🇸 [English](README.md) | 🇷🇺 [Russian](README_RU.md) |
| ----------------------- | -------------------------- |
| 🇺🇸 [English](README.md) | 🇷🇺 [Russian](README_RU.md) | 🇵🇹 [Portuguese](README_PT.md) |
| ----------------------- | -------------------------- | ---------------------------- |
Клиент Minecraft, запущенный на веб-странице. **Демонстрация на https://webclient.prismarine.js.org/**

BIN
assets/click_stereo.ogg Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
extra-textures/edition.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

BIN
extra-textures/widgets.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -17,14 +17,14 @@
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
</head>
<body>
<game-menu id="game-menu" style="display: none;"></game-menu>
<debug-menu id="debugmenu" style="display: none;"></debug-menu>
<player-list id="playerlist" style="display: none;"></player-list>
<cross-hair id="crosshair" style="display: none;"></cross-hair>
<chat-box id="chatbox" style="display: none;"></chat-box>
<health-bar id="healthbar" style="display: none;"></health-bar>
<hot-bar id="hotbar" style="display: none;"></hot-bar>
<loading-screen id="loading-background" style="display: none;"></loading-screen>
<prismarine-menu id="prismarine-menu"></prismarine-menu>
<div id="ui-root">
<pmui-hud id="hud" style="display: none;"></pmui-hud>
<pmui-pausescreen id="pause-screen" style="display: none;"></pmui-pausescreen>
<pmui-loadingscreen id="loading-screen" style="display: none;"></pmui-loadingscreen>
<pmui-playscreen id="play-screen" style="display: none;"></pmui-playscreen>
<pmui-keybindsscreen id="keybinds-screen" style="display: none;"></pmui-keybindsscreen>
<pmui-optionsscreen id="options-screen" style="display: none;"></pmui-optionsscreen>
<pmui-titlescreen id="title-screen" style="display: block;"></pmui-titlescreen>
</div>
</body>
</html>

257
index.js
View file

@ -1,13 +1,22 @@
/* global THREE */
require('./lib/menu')
require('./lib/loading_screen')
require('./lib/healthbar')
require('./lib/hotbar')
require('./lib/gameMenu')
require('./lib/chat')
require('./lib/crosshair')
require('./lib/playerlist')
require('./lib/debugmenu')
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/hud')
require('./lib/menus/play_screen')
require('./lib/menus/pause_screen')
require('./lib/menus/loading_screen')
require('./lib/menus/keybinds_screen')
require('./lib/menus/options_screen')
require('./lib/menus/title_screen')
const net = require('net')
const Cursor = require('./lib/cursor')
@ -45,43 +54,60 @@ document.body.appendChild(renderer.domElement)
const viewer = new Viewer(renderer)
// Menu panorama background
function getPanoramaMesh () {
const geometry = new THREE.SphereGeometry(500, 60, 40)
geometry.scale(-1, 1, 1)
const texture = new THREE.TextureLoader().load('title_blured.jpg')
const material = new THREE.MeshBasicMaterial({ map: texture })
const mesh = new THREE.Mesh(geometry, material)
mesh.rotation.y = Math.PI
mesh.onBeforeRender = () => {
mesh.rotation.y += 0.0005
mesh.rotation.x = -Math.sin(mesh.rotation.y * 3) * 0.3
function addPanoramaCubeMap () {
let time = 0
viewer.camera = new THREE.PerspectiveCamera(85, window.innerWidth / window.innerHeight, 0.05, 1000)
viewer.camera.updateProjectionMatrix()
viewer.camera.position.set(0, 0, 0)
viewer.camera.rotation.set(0, 0, 0)
const panorGeo = new THREE.BoxGeometry(1000, 1000, 1000)
const loader = new THREE.TextureLoader()
const panorMaterials = [
new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_1.png'), transparent: true, side: THREE.DoubleSide }), // WS
new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_3.png'), transparent: true, side: THREE.DoubleSide }), // ES
new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_4.png'), transparent: true, side: THREE.DoubleSide }), // Up
new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_5.png'), transparent: true, side: THREE.DoubleSide }), // Down
new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_0.png'), transparent: true, side: THREE.DoubleSide }), // NS
new THREE.MeshBasicMaterial({ map: loader.load('extra-textures/background/panorama_2.png'), transparent: true, side: THREE.DoubleSide }) // SS
]
const panoramaBox = new THREE.Mesh(panorGeo, panorMaterials)
panoramaBox.onBeforeRender = () => {
time += 0.01
panoramaBox.rotation.y = Math.PI + time * 0.01
panoramaBox.rotation.z = Math.sin(-time * 0.001) * 0.001
}
const group = new THREE.Object3D()
group.add(mesh)
group.add(panoramaBox)
const Entity = require('prismarine-viewer/viewer/lib/entity/Entity')
for (let i = 0; i < 42; i++) {
const m = new Entity('1.16.4', 'squid').mesh
m.position.set(Math.random() * 20 - 10, Math.random() * 20 - 10, Math.random() * 20 - 30)
m.position.set(Math.random() * 30 - 15, Math.random() * 20 - 10, Math.random() * 10 - 17)
m.rotation.set(0, Math.PI + Math.random(), -Math.PI / 4, 'ZYX')
const v = Math.random() * 0.01
m.children[0].onBeforeRender = () => {
m.rotation.y += v
m.rotation.z = Math.cos(mesh.rotation.y * 3) * Math.PI / 4 - Math.PI / 2
m.rotation.z = Math.cos(panoramaBox.rotation.y * 3) * Math.PI / 4 - Math.PI / 2
}
group.add(m)
}
viewer.scene.add(group)
return group
}
const panoramaCubeMap = addPanoramaCubeMap()
function removePanorama () {
viewer.scene.remove(panoramaMesh)
panoramaMesh = null
viewer.camera = new THREE.PerspectiveCamera(document.getElementById('options-screen').fov, window.innerWidth / window.innerHeight, 0.1, 1000)
viewer.camera.updateProjectionMatrix()
viewer.scene.remove(panoramaCubeMap)
}
let panoramaMesh = getPanoramaMesh()
viewer.scene.add(panoramaMesh)
// Browser animation loop
let animate = () => {
window.requestAnimationFrame(animate)
viewer.update()
@ -89,59 +115,36 @@ let animate = () => {
}
animate()
const calcGuiScale = (guiScaleIn) => {
let i
for (i = 1; i !== guiScaleIn && i < window.innerWidth && i < (window.innerHeight) && window.innerWidth / (i + 1) >= 320 && (window.innerHeight) / (i + 1) >= 240; ++i);
return i
}
const setScaleFactor = (value) => {
const i = calcGuiScale(value)
document.documentElement.style.setProperty('--guiScaleFactor', i)
console.log(`Scale: ${i}`)
}
window.setScaleFactor = (value) => {
setScaleFactor(value)
}
setScaleFactor(3)
window.addEventListener('resize', () => {
viewer.camera.aspect = window.innerWidth / window.innerHeight
viewer.camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
setScaleFactor(3)
})
const showEl = (str) => { document.getElementById(str).style = 'display:block' }
async function main () {
const showEl = (str) => { document.getElementById(str).style = 'display:block' }
const menu = document.getElementById('prismarine-menu')
const menu = document.getElementById('play-screen')
menu.addEventListener('connect', e => {
const options = e.detail
menu.style = 'display: none;'
showEl('healthbar')
showEl('hotbar')
showEl('crosshair')
showEl('chatbox')
showEl('loading-background')
showEl('playerlist')
showEl('debugmenu')
showEl('loading-screen')
removePanorama()
connect(options)
})
}
async function connect (options) {
const loadingScreen = document.getElementById('loading-background')
const healthbar = document.getElementById('healthbar')
const hotbar = document.getElementById('hotbar')
const chat = document.getElementById('chatbox')
const playerList = document.getElementById('playerlist')
const debugMenu = document.getElementById('debugmenu')
const gameMenu = document.getElementById('game-menu')
const loadingScreen = document.getElementById('loading-screen')
const viewDistance = 6
const hud = document.getElementById('hud')
const chat = hud.shadowRoot.querySelector('#chat')
const debugMenu = hud.shadowRoot.querySelector('#debug-overlay')
const optionsScrn = document.getElementById('options-screen')
const keyBindScrn = document.getElementById('keybinds-screen')
const gameMenu = document.getElementById('pause-screen')
const viewDistance = optionsScrn.renderDistance
const hostprompt = options.server
const proxyprompt = options.proxy
const username = options.username
@ -183,26 +186,25 @@ async function connect (options) {
closeTimeout: 240 * 1000
})
healthbar.bot = bot
hotbar.bot = bot
debugMenu.bot = bot
bot.on('error', (err) => {
console.log('Encountered error!', err)
loadingScreen.status = `Error encountered. Error message: ${err}. Please reload the page`
loadingScreen.style = 'display: block;'
loadingScreen.hasError = true
})
bot.on('kicked', (kickReason) => {
console.log('User was kicked!', kickReason)
loadingScreen.status = `The Minecraft server kicked you. Kick reason: ${kickReason}. Please reload the page to rejoin`
loadingScreen.style = 'display: block;'
loadingScreen.hasError = true
})
bot.on('end', (endReason) => {
console.log('disconnected for', endReason)
loadingScreen.status = `You have been disconnected from the server. End reason: ${endReason}. Please reload the page to rejoin`
loadingScreen.style = 'display: block;'
loadingScreen.hasError = true
})
bot.once('login', () => {
@ -212,7 +214,7 @@ async function connect (options) {
bot.once('spawn', () => {
const mcData = require('minecraft-data')(bot.version)
loadingScreen.status = 'Placing blocks (starting viewer)...'
loadingScreen.status = 'Placing blocks (starting viewer)'
console.log('bot spawned - starting viewer')
@ -220,15 +222,18 @@ async function connect (options) {
const center = bot.entity.position
console.log(viewDistance)
const worldView = new WorldView(bot.world, viewDistance, center)
chat.init(bot._client, renderer)
gameMenu.init(renderer)
playerList.init(bot)
optionsScrn.isInsideWorld = true
optionsScrn.addEventListener('fov_changed', (e) => {
viewer.camera.fov = e.detail.fov
viewer.camera.updateProjectionMatrix()
})
viewer.setVersion(version)
hotbar.viewerVersion = viewer.version
window.worldView = worldView
window.bot = bot
window.mcData = mcData
@ -236,15 +241,8 @@ async function connect (options) {
window.Vec3 = Vec3
window.pathfinder = pathfinder
window.debugMenu = debugMenu
window.settings = optionsScrn
window.renderer = renderer
window.settings = {
mouseSensXValue: window.localStorage.getItem('mouseSensX') ?? 0.005,
mouseSensYValue: window.localStorage.getItem('mouseSensY') ?? 0.005,
set mouseSensX (v) { this.mouseSensXValue = v; window.localStorage.setItem('mouseSensX', v) },
set mouseSensY (v) { this.mouseSensYValue = v; window.localStorage.setItem('mouseSensY', v) },
get mouseSensX () { return this.mouseSensXValue },
get mouseSensY () { return this.mouseSensYValue }
}
initVR(bot, renderer, viewer)
@ -270,12 +268,12 @@ async function connect (options) {
bot.on('move', botPosition)
botPosition()
loadingScreen.status = 'Setting callbacks...'
loadingScreen.status = 'Setting callbacks'
function moveCallback (e) {
bot.entity.pitch -= e.movementY * window.settings.mouseSensYValue
bot.entity.pitch -= e.movementY * optionsScrn.mouseSensitivityY * 0.0001
bot.entity.pitch = Math.max(minPitch, Math.min(maxPitch, bot.entity.pitch))
bot.entity.yaw -= e.movementX * window.settings.mouseSensXValue
bot.entity.yaw -= e.movementX * optionsScrn.mouseSensitivityX * 0.0001
viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
}
@ -320,16 +318,6 @@ async function connect (options) {
document.addEventListener('contextmenu', (e) => e.preventDefault(), false)
const codes = {
KeyW: 'forward',
KeyS: 'back',
KeyA: 'right',
KeyD: 'left',
Space: 'jump',
ShiftLeft: 'sneak',
ControlLeft: 'sprint'
}
window.addEventListener('blur', (e) => {
bot.clearControlStates()
}, false)
@ -337,42 +325,79 @@ async function connect (options) {
document.addEventListener('keydown', (e) => {
if (chat.inChat) return
if (gameMenu.inMenu) return
console.log(e.code)
if (e.code in codes) {
bot.setControlState(codes[e.code], true)
}
if (e.code.startsWith('Digit')) {
const numPressed = e.code.substr(5)
if (numPressed < 1 || numPressed > 9) return
hotbar.reloadHotbarSelected(numPressed - 1)
}
if (e.code === 'KeyQ') {
if (bot.heldItem) bot.tossStack(bot.heldItem)
}
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('left', true)
break
case 'KeyA':
bot.setControlState('right', true)
break
case 'KeyS':
bot.setControlState('back', true)
break
case 'KeyW':
bot.setControlState('forward', true)
break
}
}
})
}, false)
document.addEventListener('keyup', (e) => {
if (e.code in codes) {
bot.setControlState(codes[e.code], false)
}
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('left', false)
break
case 'KeyA':
bot.setControlState('right', false)
break
case 'KeyS':
bot.setControlState('back', false)
break
case 'KeyW':
bot.setControlState('forward', false)
break
}
}
})
}, false)
loadingScreen.status = 'Done!'
console.log(loadingScreen.status) // only do that because it's read in index.html and npm run fix complains.
hud.init(renderer, bot, host)
hud.style.display = 'block'
setTimeout(function () {
// remove loading screen, wait a second to make sure a frame has properly rendered
loadingScreen.style = 'display: none;'
}, 2500)
// TODO: Remove after #85 is done
debugMenu.customEntries.food = bot.food
debugMenu.customEntries.saturation = bot.foodSaturation
bot.on('health', () => {
debugMenu.customEntries.food = bot.food
debugMenu.customEntries.saturation = bot.foodSaturation
})
})
}
main()

View file

@ -40,19 +40,22 @@ class ChatBox extends LitElement {
return css`
.chat-wrapper {
position: fixed;
z-index:10;
z-index: 10;
}
.chat-display-wrapper {
bottom: calc(8px * 16);
bottom: 40px;
padding: 4px;
max-height: calc(90px * 8);
width: calc(320px * 4);
padding-left: 0;
max-height: var(--chatHeight);
width: var(--chatWidth);
}
.chat-input-wrapper {
bottom: calc(2px * 16);
width: 100%;
bottom: 2px;
width: calc(100% - 2px);
position: relative;
left: 1px;
overflow: hidden;
background-color: rgba(0, 0, 0, 0);
}
@ -60,22 +63,29 @@ class ChatBox extends LitElement {
.chat {
overflow: hidden;
color: white;
font-size: 16px;
font-size: 10px;
margin: 0px;
line-height: 100%;
text-shadow: 2px 2px 0px #3f3f3f;
text-shadow: 1px 1px 0px #3f3f3f;
font-family: mojangles, minecraft, monospace;
width: 100%;
max-height: calc(90px * 8);
max-height: var(--chatHeight);
}
input[type=text], #chatinput {
background-color: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(0, 0, 0, 0.5);
display: none;
outline: none;
}
#chatinput:focus {
border-color: white;
}
.chat-message {
display: block;
padding-left: 4px;
background-color: rgba(0, 0, 0, 0.5);
}
@ -102,13 +112,13 @@ class ChatBox extends LitElement {
render () {
return html`
<div id="chat-wrapper" class="chat-wrapper chat-display-wrapper">
<div class="chat" id="chat">
<li class="chat-message chat-message-fade chat-message-faded">Welcome to prismarine-web-client! Chat appears here.</li>
</div>
<div class="chat" id="chat">
<li class="chat-message chat-message-fade chat-message-faded">Welcome to prismarine-web-client! Chat appears here.</li>
</div>
</div>
<div id="chat-wrapper2" class="chat-wrapper chat-input-wrapper">
<div class="chat" id="chat-input">
<input type="text" class="chat" id="chatinput"></input>
<input type="text" class="chat" id="chatinput" spellcheck="false" autocomplete="off"></input>
</div>
</div>
`
@ -117,7 +127,7 @@ class ChatBox extends LitElement {
init (client, renderer) {
this.inChat = false
const chat = this.shadowRoot.querySelector('#chat')
const gameMenu = document.getElementById('game-menu')
const gameMenu = document.getElementById('pause-screen')
const chatInput = this.shadowRoot.querySelector('#chatinput')
const chatHistory = []
@ -132,12 +142,12 @@ class ChatBox extends LitElement {
const self = this
// Esc event - Doesnt work with onkeypress?!
// Esc event - Doesnt work with onkeypress?! - keypressed is deprecated uk
document.addEventListener('keydown', e => {
if (gameMenu.inMenu) return
if (!self.inChat) return
e = e || window.event
if (e.keyCode === 27 || e.key === 'Escape' || e.key === 'Esc') {
if (e.code === 'Escape') {
disableChat()
} else if (e.keyCode === 38) {
if (chatHistoryPos === 0) return
@ -148,18 +158,26 @@ class ChatBox extends LitElement {
}
})
const keyBindScrn = document.getElementById('keybinds-screen')
// Chat events
document.addEventListener('keypress', e => {
if (gameMenu.inMenu) return
e = e || window.event
if (self.inChat === false) {
if (e.code === 'KeyT') {
setTimeout(() => enableChat(false), 0)
}
keyBindScrn.keymaps.forEach(km => {
if (e.code === km.key) {
switch (km.defaultKey) {
case 'KeyT':
setTimeout(() => enableChat(false), 0)
break
case 'Slash':
setTimeout(() => enableChat(true), 0)
break
}
}
})
if (e.code === 'Slash') {
setTimeout(() => enableChat(true), 0)
}
return false
}
@ -191,7 +209,7 @@ class ChatBox extends LitElement {
// Show chat input
chatInput.style.display = 'block'
// Show extended chat history
chat.style.maxHeight = 'calc(90px * 8)'
chat.style.maxHeight = 'calc(var(--chatHeight) * var(--chatScale))'
chat.scrollTop = chat.scrollHeight // Stay bottom of the list
if (isCommand) { // handle commands
chatInput.value = '/'
@ -199,16 +217,12 @@ class ChatBox extends LitElement {
// Focus element
chatInput.focus()
chatHistoryPos = chatHistory.length
document.querySelector('#chatbox').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.add('chat-message-chat-opened'))
document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.add('chat-message-chat-opened'))
}
function disableChat () {
// Set inChat value
self.inChat = false
// Hide chat
hideChat()
renderer.domElement.requestPointerLock()
}
@ -220,9 +234,9 @@ class ChatBox extends LitElement {
// Hide it
chatInput.style.display = 'none'
// Hide extended chat history
chat.style.maxHeight = 'calc(90px * 4)'
chat.style.maxHeight = 'var(--chatHeight)'
chat.scrollTop = chat.scrollHeight // Stay bottom of the list
document.querySelector('#chatbox').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.remove('chat-message-chat-opened'))
document.querySelector('#hud').shadowRoot.querySelector('#chat').shadowRoot.querySelectorAll('.chat-message').forEach(e => e.classList.remove('chat-message-chat-opened'))
}
function readExtra (extra) {

View file

@ -1,112 +0,0 @@
const { LitElement, html, css } = require('lit')
export class LegacyButton extends LitElement {
static get styles () {
return css`
:host {
--guiScale: var(--guiScaleFactor, 3);
}
.legacy-btn {
--textColor: white;
image-rendering: crisp-edges;
image-rendering: pixelated;
text-decoration: none;
cursor: default;
border: none;
background: none;
outline: none;
position: relative;
z-index: 1;
display: grid;
width: 100%;
height: calc(20px * var(--guiScale));
font-family: mojangles, minecraft, monospace;
font-size: calc(10px * var(--guiScale));
align-items: center;
justify-content: center;
color: var(--textColor);
text-shadow: calc(1px * var(--guiScale)) calc(1px * var(--guiScale)) black;
}
.legacy-btn:disabled {
--textColor: rgb(160, 160, 160);
}
.legacy-btn::after,
.legacy-btn::before {
--yPos: -66px;
content: '';
display: block;
position: absolute;
top: 0;
width: 50%;
height: 100%;
z-index: -1;
background-image: url('textures/1.16.4/gui/widgets.png');
background-size: calc(256px * var(--guiScale));
background-position-y: calc(var(--yPos) * var(--guiScale));
}
.legacy-btn::after {
left: 0;
}
.legacy-btn::before {
left: 50%;
background-position-x: calc(-200px * var(--guiScale) + 100%);
}
.legacy-btn:hover::after,
.legacy-btn:hover::before,
.legacy-btn:focus::after,
.legacy-btn:focus::before {
--yPos: -86px;
}
.legacy-btn:disabled::after,
.legacy-btn:disabled::before {
--yPos: -46px;
--textColor: rgb(160, 160, 160);
}
`
}
static get properties () {
return {
size: {
type: String,
attribute: 'btn-width'
},
scaleFactor: {
type: Number,
attribute: 'scale-factor'
}
}
}
constructor () {
super()
this.scaleFactor = 3
this.size = '100%'
this.offset = [0, 0]
}
render () {
return html`
<button class="legacy-btn" style="width: calc(${this.size.endsWith('%') ? this.size : this.size + ' * var(--guiScale)'});"><slot></slot></button>
`
}
}
window.customElements.define('legacy-button', LegacyButton)

View file

@ -1,30 +0,0 @@
const { html } = require('lit')
const { LegacyButton } = require('./button')
class LegacyButtonLink extends LegacyButton {
constructor () {
super()
this.href = ''
}
static get properties () {
return {
size: {
type: String,
attribute: 'btn-width'
},
href: {
type: String,
attribute: 'go-to'
}
}
}
render () {
return html`
<a class="legacy-btn" href=${this.href} target="_blank" style="width: calc(${this.size} * var(--guiScale));"><slot></slot></a>
`
}
}
window.customElements.define('legacy-button-link', LegacyButtonLink)

View file

@ -1,106 +0,0 @@
const { LitElement, html, css } = require('lit')
class LegacyTextField extends LitElement {
constructor () {
super()
this.size = '200px'
this.id = ''
this.value = ''
this.label = ''
}
static get properties () {
return {
size: {
type: String,
attribute: 'field-width'
},
label: {
type: String,
attribute: 'field-label'
},
id: {
type: String,
attribute: 'field-id'
},
value: {
type: String,
attribute: 'field-value'
}
}
}
static get styles () {
return css`
:host {
--guiScale: var(--guiScaleFactor, 3);
}
.text-field-div {
--borderColor: grey;
position: relative;
padding: 0;
margin: 0;
width: 100%;
height: calc(22px * var(--guiScale));
box-sizing: border-box;
background: black;
border: calc(1px * var(--guiScale)) solid var(--borderColor);
padding: calc(4px * var(--guiScale));
}
.text-field-div:focus-within {
--borderColor: white;
}
.text-field-div label {
position: absolute;
z-index: 2;
pointer-events: none;
bottom: calc(21px * var(--guiScale));
left: 0;
padding: 0;
margin: 0;
font-family: mojangles, minecraft, monospace;
font-size: calc(10px * var(--guiScale));
color: rgb(206, 206, 206);
text-shadow: calc(1px * var(--guiScale)) calc(1px * var(--guiScale)) black;
}
.legacy-text-field {
outline: none;
border: none;
background: none;
padding: 0;
margin: 0;
width: 100%;
height: 100%;
font-family: mojangles, minecraft, monospace;
font-size: calc(8px * var(--guiScale));
color: white;
text-shadow: calc(1px * var(--guiScale)) calc(1px * var(--guiScale)) black;
}
`
}
render () {
return html`
<div class="text-field-div" style="width: calc(${this.size.endsWith('%') ? this.size : this.size + ' * var(--guiScale)'});">
<label for="${this.id}">${this.label}</label>
<input id="${this.id}" type="text" name="" spellcheck="false" required="" autocomplete="off" value="${this.value}" @input=${(e) => { this.value = e.target.value }} class="legacy-text-field">
</div>
`
}
}
window.customElements.define('legacy-text-field', LegacyTextField)

View file

@ -1,29 +0,0 @@
const { LitElement, html, css } = require('lit')
class CrossHair extends LitElement {
static get styles () {
return css`
#crosshair {
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: -o-crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
position: absolute;
top: 50%;
left: 50%;
height: calc(256px * 4);
width: calc(256px * 4);
transform: translate(calc(-50% + 120px * 4), calc(-50% + 120px * 4));
clip-path: inset(0px calc(240px * 4) calc(240px * 4) 0px);
z-index:10;
}
`
}
render () {
return html`<img id="crosshair" src="extra-textures/icons.png">`
}
}
window.customElements.define('cross-hair', CrossHair)

View file

@ -67,14 +67,14 @@ class Cursor {
this.breakStartTime = performance.now()
try {
bot.dig(cursorBlock, 'ignore')
} catch {} // we don't care if its aborted
} catch (e) {} // we don't care if its aborted
}
// Stop break
if (!this.buttons[0] && this.lastButtons[0]) {
try {
bot.stopDigging() // this shouldnt throw anything...
} catch {} // to be reworked in mineflayer, then remove the try here
} catch (e) {} // to be reworked in mineflayer, then remove the try here
}
// Show break animation

View file

@ -1,118 +0,0 @@
const { LitElement, html, css } = require('lit')
class DebugMenu extends LitElement {
constructor () {
super()
this.isOpen = false
this.customEntries = {}
}
firstUpdated () {
document.addEventListener('keydown', e => {
e ??= window.event
if (e.key === 'F3') {
this.isOpen = !this.isOpen
e.preventDefault()
}
})
}
static get styles () {
return css`
.debugmenu-wrapper {
position: fixed;
z-index:25;
}
.debugmenu {
overflow: hidden;
color: white;
font-size: 16px;
margin: 0px;
line-height: 100%;
text-shadow: 2px 2px 0px #3f3f3f;
font-family: mojangles, minecraft, monospace;
width: calc(320px * 4);
max-height: calc(90px * 8);
top: calc(8px * 16);
padding: 4px;
}
.debugmenu p {
margin: 0px;
padding: 1px;
width: fit-content;
background-color: rgba(0, 0, 0, 0.5);
}
`
}
static get properties () {
return {
isOpen: { type: Boolean },
cursorBlock: { type: Object },
bot: { type: Object },
customEntries: { type: Object }
}
}
updated (changedProperties) {
if (changedProperties.has('bot')) {
this.bot.on('move', () => {
this.requestUpdate()
})
}
}
render () {
if (!this.isOpen) {
return html``
}
const target = this.cursorBlock
const targetDiggable = (target && this.bot.canDigBlock(target))
const pos = this.bot.entity.position
const rot = [this.bot.entity.yaw, this.bot.entity.pitch]
const viewDegToMinecraft = (yaw) => {
return yaw % 360 - 180 * (yaw < 0 ? -1 : 1)
}
const quadsDescription = [
'north (towards negative Z)',
'east (towards positive X)',
'south (towards positive Z)',
'west (towards negative X)'
]
const minecraftYaw = viewDegToMinecraft(rot[0] * -180 / Math.PI)
const minecraftQuad = Math.floor(((minecraftYaw + 180) / 90 + 0.5) % 4)
const renderProp = (name, value, nextItem) => {
return html`${name}: ${typeof value === 'boolean'
? html`<span style="color: ${value ? 'lightgreen' : 'red'}">${value}</span>`
: value}${nextItem ? ' / ' : ''}`
}
return html`
<div id="debugmenu-wrapper" class="debugmenu-wrapper">
<div class="debugmenu" id="debugmenu">
<p id="debug-entry-renderer">Renderer: three.js r${global.THREE.REVISION}</p>
</br>
<p>XYZ: ${pos.x.toFixed(3)} / ${pos.y.toFixed(3)} / ${pos.z.toFixed(3)}</p>
<p>Chunk: ${Math.floor(pos.x % 16)} ~ ${Math.floor(pos.z % 16)} in ${Math.floor(pos.x / 16)} ~ ${Math.floor(pos.z / 16)}</p>
<p>Facing (viewer): ${rot[0].toFixed(3)} ${rot[1].toFixed(3)}</p>
<p>Facing (minecraft): ${quadsDescription[minecraftQuad]} (${minecraftYaw.toFixed(1)} ${(rot[1] * -180 / Math.PI).toFixed(1)})</p>
${targetDiggable ? html`<p>Looking at: ${target.position.x} ${target.position.y} ${target.position.z}</p>` : ''}
${targetDiggable ? html`<p>${target.name}${Object.entries(target.getProperties()).length > 0 ? ' | ' : ''}${Object.entries(target.getProperties()).map(([n, p], idx, arr) => renderProp(n, p, arr[idx + 1]))}</p>` : ''}<br>
${Object.entries(this.customEntries).map(([name, value]) => html`<p>${name}: ${value}</p>`)}
</div>
</div>
`
}
}
window.customElements.define('debug-menu', DebugMenu)

View file

@ -1,129 +0,0 @@
const { LitElement, html, css } = require('lit')
require('./github_link')
require('./components/button')
require('./components/buttonlink')
require('./components/textfield')
class GameMenu extends LitElement {
constructor () {
super()
this.inMenu = false
}
disableGameMenu (renderer = false) {
this.inMenu = false
this.style.display = 'none'
if (renderer) {
renderer.domElement.requestPointerLock()
}
}
enableGameMenu () {
this.inMenu = true
document.exitPointerLock()
this.style.display = 'block'
this.focus()
}
static get styles () {
return css`
:host {
--guiScale: var(--guiScaleFactor, 3);
}
html {
height: 100%;
}
body {
margin:0;
padding:0;
font-family: sans-serif;
background: #000;
}
.menu-box {
position: fixed;
z-index: 11;
top: 50%;
left: 50%;
width: calc(180px * var(--guiScale));
padding: calc(10px * var(--guiScale));
transform: translate(-50%, -50%);
box-sizing: border-box;
border-radius: 10px;
background: rgba(0, 0, 0, 0.8)
}
.link-buttons {
display: flex;
justify-content: space-between;
gap: calc(4px * var(--guiScale));
}
.title, .subtitle {
text-align: center;
font-family: mojangles, minecraft, monospace;
font-size: calc(10px * var(--guiScale));
font-weight: normal;
color: white;
margin-top: 0;
text-shadow: calc(1px * var(--guiScale)) calc(1px * var(--guiScale)) black;
}
.subtitle {
font-size: calc(7.5px * var(--guiScale));
}
.wrapper {
display: flex;
justify-content: space-between;
gap: calc(6px * var(--guiScale));
}
.spacev {
height: calc(6px * var(--guiScale));
}
.field-spacev {
height: calc(14px * var(--guiScale));
}
`
}
render () {
return html`
<github-link></github-link>
<div class="menu-box">
<h2 class="title">Game Menu</h2>
<div class="spacev"></div>
<legacy-button btn-width="100%" @click=${() => { this.disableGameMenu() }}>Back to Game</legacy-button>
<div class="spacev"></div>
<legacy-button btn-width="100%">Options</legacy-button>
<div class="spacev"></div>
<legacy-button btn-width="100%" onClick="window.location.reload();">Disconnect</legacy-button>
</div>
`
}
init (renderer) {
const chat = document.getElementById('chatbox')
const self = this
document.addEventListener('keydown', e => {
if (chat.inChat) return
e = e || window.event
if (e.keyCode === 27 || e.key === 'Escape' || e.key === 'Esc') {
if (self.inMenu) {
self.disableGameMenu(renderer)
} else {
self.enableGameMenu()
}
}
})
}
}
window.customElements.define('game-menu', GameMenu)

View file

@ -1,58 +0,0 @@
const { LitElement, html, css } = require('lit')
class GithubLink extends LitElement {
static get styles () {
return css`
.github-corner:hover .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
0% {
transform: rotate(0deg);
}
20% {
transform: rotate(-25deg);
}
40% {
transform: rotate(10deg);
}
60% {
transform: rotate(-25deg);
}
80% {
transform: rotate(10deg);
}
100% {
transform: rotate(0deg);
}
}
@media (max-width: 500px) {
.github-corner:hover .octo-arm {
animation: none;
}
.github-corner .octo-arm {
animation: octocat-wave 560ms ease-in-out;
}
}
`
}
render () {
return html`<a href="https://github.com/PrismarineJS/prismarine-web-client" class="github-corner" aria-label="View source on GitHub">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true">
<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>
</a>`
}
}
window.customElements.define('github-link', GithubLink)

View file

@ -1,224 +0,0 @@
const { LitElement, html, css } = require('lit')
function getEffectClass (effect) {
switch (effect.id) {
case 19: return 'poisoned'
case 20: return 'withered'
case 22: return 'absorption'
default: return ''
}
}
class HealthBar extends LitElement {
updated (changedProperties) {
if (changedProperties.has('bot')) {
this.bot.once('spawn', () => this.init())
}
}
init () {
this.bot.on('entityHurt', (entity) => {
if (entity !== this.bot.entity) return
this.onDamage()
})
this.bot.on('entityEffect', (entity, effect) => {
if (entity !== this.bot.entity) return
this.shadowRoot.firstElementChild.classList.add(getEffectClass(effect))
})
this.bot.on('entityEffectEnd', (entity, effect) => {
if (entity !== this.bot.entity) return
this.shadowRoot.firstElementChild.classList.remove(getEffectClass(effect))
})
this.bot.on('game', () => this.onGameUpdate())
this.bot.on('health', () => this.updateHealth())
this.onGameUpdate()
this.updateHealth()
}
onGameUpdate () {
this.shadowRoot.firstElementChild.classList.toggle('creative', this.bot.player.gamemode === 1)
this.shadowRoot.firstElementChild.classList.toggle('hardcore', this.bot.game.hardcore)
}
onDamage () {
this.shadowRoot.firstElementChild.classList.add('damaged')
if (this.hurtTimeout) clearTimeout(this.hurtTimeout)
this.hurtTimeout = setTimeout(() => {
this.shadowRoot.firstElementChild.classList.remove('damaged')
this.hurtTimeout = null
}, 1000)
}
updateHealth () {
const wrapper = this.shadowRoot.firstElementChild
const health = wrapper.firstElementChild
const absorption = wrapper.lastElementChild
health.dataset.value = Math.min(this.bot.health, 20)
absorption.dataset.value = Math.max(this.bot.health - 20, 0)
health.classList.toggle('low', this.bot.health <= 4)
}
static get properties () {
return {
bot: { type: Object }
}
}
static get styles () {
return css`
#healthbar {
position: absolute;
top: 100%;
left: 50%;
transform: translate(calc(-182px * 2), calc(-22px * 7));
display: flex;
flex-direction: column-reverse;
--lightened: 0;
--hardcore: 0;
--kind: 1;
--background-x: calc(-3 * (16px + var(--lightened) * 9px));
--background-y: calc(-3 * (var(--hardcore) * 5 * 9px));
--offset: calc(-3 * (16px + 9px * (var(--kind) * 4 + var(--lightened) * 2)));
--health: attr(data-value number);
}
#healthbar.creative { display: none; }
#healthbar.hardcore { --hardcore: 1; }
#healthbar.poisoned { --kind: 2; }
#healthbar.withered { --kind: 3; }
.health.absorption { --kind: 4; }
.health.absorption > * { visibility: hidden; }
.health > * , .food > * {
display: inline-block;
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: -o-crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
width: calc(3 * 9px);
height: calc(3 * 9px);
margin: 0;
margin-right: -3px;
background-image: url(extra-textures/icons.png), url(extra-textures/icons.png);
background-repeat: no-repeat, no-repeat;
background-size: calc(3 * 256px) auto, calc(3 * 256px) auto;
background-position: var(--background-x) var(--background-y), var(--background-x) var(--background-y);
}
/* Damage flashing animation */
.health.damaged {
animation: damaged 0.3s steps(2, end) 2;
}
@media (prefers-reduced-motion) {
.health.damaged {
animation: none;
--lightened: 1;
}
}
@keyframes damaged {
to { --lightened: 1; }
}
/* Low health shaking animation */
.health.low > * {
animation: lowhealth 0.2s steps(2, end) infinite;
}
.health.low > *:nth-child(2n) {
animation-direction: reverse;
}
.health.low > *:nth-child(3n) {
animation-duration: 0.1s;
}
@media (prefers-reduced-motion) {
.health.low > * {
animation-duration: 0.5s !important;
}
}
@keyframes lowhealth {
to {
transform: translateY(3px);
}
}
/* 1 - 2-20 */
.health[data-value*="2"] > :nth-child(1),
.health[data-value*="3"] > :nth-child(1),
.health[data-value*="4"] > :nth-child(1),
.health[data-value*="5"] > :nth-child(1),
.health[data-value*="6"] > :nth-child(1),
.health[data-value*="7"] > :nth-child(1),
.health[data-value*="8"] > :nth-child(1),
.health[data-value*="9"] > :nth-child(1),
.health[data-value^="1"]:not([data-value="1"]) > :nth-child(1),
/* 2 - 4-20 */
.health[data-value*="4"] > :nth-child(2),
.health[data-value*="5"] > :nth-child(2),
.health[data-value*="6"] > :nth-child(2),
.health[data-value*="7"] > :nth-child(2),
.health[data-value*="8"] > :nth-child(2),
.health[data-value*="9"] > :nth-child(2),
.health[data-value^="1"]:not([data-value="1"]) > :nth-child(2),
.health[data-value="20"] > :nth-child(2),
/* 3 - 6-20 */
.health[data-value*="6"] > :nth-child(3),
.health[data-value*="7"] > :nth-child(3),
.health[data-value*="8"] > :nth-child(3),
.health[data-value*="9"] > :nth-child(3),
.health[data-value^="1"]:not([data-value="1"]) > :nth-child(3),
.health[data-value="20"] > :nth-child(3),
/* 4 - 8-20 */
.health[data-value*="8"] > :nth-child(4),
.health[data-value*="9"] > :nth-child(4),
.health[data-value^="1"]:not([data-value="1"]) > :nth-child(4),
.health[data-value="20"] > :nth-child(4),
/* 5 - 10-20 */
.health[data-value^="1"]:not([data-value="1"]) > :nth-child(5),
.health[data-value="20"] > :nth-child(5),
/* 6 - 12-20 */
.health[data-value^="1"]:not([data-value$="1"]):not([data-value$="0"]) > :nth-child(6),
.health[data-value="20"] > :nth-child(6),
/* 7 - 14-20 */
.health[data-value^="1"]:not([data-value$="0"]):not([data-value$="1"]):not([data-value$="2"]):not([data-value$="3"]) > :nth-child(7),
.health[data-value="20"] > :nth-child(7),
/* 8 - 16-20 */
.health[data-value="16"] > :nth-child(8),
.health[data-value="17"] > :nth-child(8),
.health[data-value="18"] > :nth-child(8),
.health[data-value="19"] > :nth-child(8),
.health[data-value="20"] > :nth-child(8),
/* 9 - 18-20 */
.health[data-value="18"] > :nth-child(9),
.health[data-value="19"] > :nth-child(9),
.health[data-value="20"] > :nth-child(9),
/* 10 - 20 */
.health[data-value="20"] > :nth-child(10),
.health > .full {
visibility: visible;
background-position: var(--offset) var(--background-y), var(--background-x) var(--background-y);
}
.health[data-value="1"] > :nth-child(1),
.health[data-value="3"] > :nth-child(2),
.health[data-value="5"] > :nth-child(3),
.health[data-value="7"] > :nth-child(4),
.health[data-value="9"] > :nth-child(5),
.health[data-value="11"] > :nth-child(6),
.health[data-value="13"] > :nth-child(7),
.health[data-value="15"] > :nth-child(8),
.health[data-value="17"] > :nth-child(9),
.health[data-value="19"] > :nth-child(10),
.health > .half {
visibility: visible;
background-position: calc((-3 * 9px) + var(--offset)) var(--background-y), var(--background-x) var(--background-y);
}`
}
render () {
return html`<div id="healthbar">
<div class="health" data-value="20"><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p></div>
<div class="health absorption" data-value="0"><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p><p></p></div>
</div>`
}
}
window.customElements.define('health-bar', HealthBar)

View file

@ -1,172 +0,0 @@
const { LitElement, html, css } = require('lit')
const invsprite = require('./invsprite.json')
class HotBar extends LitElement {
updated (changedProperties) {
if (changedProperties.has('bot')) {
// inventory listener
this.bot.once('spawn', () => {
this.init()
})
}
}
init () {
this.reloadHotbar()
this.reloadHotbarSelected(0)
document.addEventListener('wheel', (e) => {
const newSlot = ((this.bot.quickBarSlot + Math.sign(e.deltaY)) % 9 + 9) % 9
this.reloadHotbarSelected(newSlot)
})
this.bot.inventory.on('updateSlot', (slot, oldItem, newItem) => {
if (slot >= this.bot.inventory.hotbarStart + 9) return
if (slot < this.bot.inventory.hotbarStart) return
const sprite = newItem ? invsprite[newItem.name] : invsprite.air
const slotImage = this.shadowRoot.getElementById('hotbar-' + (slot - this.bot.inventory.hotbarStart))
slotImage.style['background-position-x'] = `-${sprite.x * 2}px`
slotImage.style['background-position-y'] = `-${sprite.y * 2}px`
slotImage.innerHTML = newItem?.count > 1 ? newItem.count : ''
})
}
async reloadHotbar () {
for (let i = 0; i < 9; i++) {
const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + i]
const sprite = item ? invsprite[item.name] : invsprite.air
const slotImage = this.shadowRoot.getElementById('hotbar-' + i)
slotImage.style['background-position-x'] = `-${sprite.x * 2}px`
slotImage.style['background-position-y'] = `-${sprite.y * 2}px`
slotImage.innerHTML = item?.count > 1 ? item.count : ''
}
}
async reloadHotbarSelected (slot) {
const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + slot]
const planned = (20 * 4 * slot) + 'px'
this.shadowRoot.getElementById('hotbar-highlight').style.marginLeft = planned
this.bot.setQuickBarSlot(slot)
this.activeItemName = item?.displayName
const name = this.shadowRoot.getElementById('hotbar-item-name')
name.classList.remove('hotbar-item-name-fader')
setTimeout(() => name.classList.add('hotbar-item-name-fader'), 10)
}
static get properties () {
return {
bot: { type: Object },
viewerVersion: { type: String },
activeItemName: { type: String }
}
}
static get styles () {
return css`
#hotbar-wrapper {
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: -o-crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
}
#hotbar-image {
position: absolute;
top: 100%;
left: 50%;
height: calc(256px * 4);
width: calc(256px * 4);
transform: translate(calc(-182px * 2), calc(-22px * 4));
clip-path: inset(0px calc(74px * 4) calc(234px * 4) 0px);
}
#hotbar-items-wrapper {
position: absolute;
top: 100%;
left: 50%;
height: calc(256px * 4);
width: calc(256px * 4);
transform: translate(calc(-182px * 2), calc(-22px * 4));
clip-path: inset(0px calc(74px * 4) calc(234px * 4) 0px);
}
#hotbar-highlight {
position: absolute;
top: 100%;
left: 50%;
height: calc(256px * 4);
width: calc(256px * 4);
margin-left: calc(20px * 4 * 4); /* EDIT THIS TO CHANGE WHICH SLOT IS SELECTED */
transform: translate(calc((-24px * 2) - (20px * 4 * 4) ), calc((-22px * 4) + (-24px * 4) + 4px)); /* first need to translate up to account for clipping, then account for size of image, then 1px to center vertically over the image*/
clip-path: inset(calc(22px * 4) calc(232px * 4) calc(210px * 4) 0px);
}
.hotbar-item {
display: inline-block;
height: 32px;
width: 64px;
margin-top: 12px;
margin-left: 12px;
margin-right: 4.25px;
background-image: url('invsprite.png');
background-size: 2048px auto;
text-align: right;
font-size: 32px;
vertical-align: top;
padding-top: 32px;
color: #ffffff;
text-shadow: 2px 2px 0px #3f3f3f;
font-family: mojangles, minecraft, monospace;
}
#hotbar-item-name {
color: white;
position: absolute;
top: 100%;
left: 50%;
transform: translate(-50%, calc(-170px));
margin-top: 0px;
text-shadow: rgb(63, 63, 63) 2px 2px 0px;
font-family: mojangles, minecraft, monospace;
font-size: 24px;
text-align: center;
}
.hotbar-item-name-fader {
opacity: 0;
transition: visibility 0s, opacity 2s linear;
transition-delay: 2s;
}
`
}
constructor () {
super()
this.activeItemName = ''
}
render () {
return html`
<div id="hotbar-wrapper">
<p id="hotbar-item-name">${this.activeItemName}</p>
<img id="hotbar-image" src="textures/1.16.4/gui/widgets.png">
<img id="hotbar-highlight" src="textures/1.16.4/gui/widgets.png">
<div id="hotbar-items-wrapper">
<div class="hotbar-item" id="hotbar-0"></div
><div class="hotbar-item" id="hotbar-1"></div
><div class="hotbar-item" id="hotbar-2"></div
><div class="hotbar-item" id="hotbar-3"></div
><div class="hotbar-item" id="hotbar-4"></div
><div class="hotbar-item" id="hotbar-5"></div
><div class="hotbar-item" id="hotbar-6"></div
><div class="hotbar-item" id="hotbar-7"></div
><div class="hotbar-item" id="hotbar-8"></div>
</div>
</div>
`
}
}
window.customElements.define('hot-bar', HotBar)

View file

@ -1,84 +0,0 @@
const { LitElement, html, css } = require('lit')
class LoadingScreen extends LitElement {
constructor () {
super()
this.status = 'Waiting for JS load'
}
firstUpdated () {
this.statusRunner()
}
static get properties () {
return {
status: { type: String },
loadingText: { type: String }
}
}
async statusRunner () {
const array = ['.', '..', '...', '']
// eslint-disable-next-line promise/param-names
const timer = ms => new Promise(res => setTimeout(res, ms))
const load = async () => {
for (let i = 0; true; i = ((i + 1) % array.length)) {
this.loadingText = this.status + array[i]
await timer(500)
}
}
load()
}
static get styles () {
return css`
h1 {
font-family: mojangles, minecraft, monospace;
}
.loader {
display: initial;
}
#loading-image {
height: 75%;
top: 50%;
left: 50%;
position: absolute;
transform: translate(-50%, -50%);
image-rendering: crisp-edges;
image-rendering: -webkit-crisp-edges;
}
#loading-background {
background-color: #60a490;
z-index: 100;
height: 100% !important;
width: 100%;
position: fixed;
}
#loading-text {
color: #29594b;
z-index: 200;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, 12rem);
}
`
}
render () {
return html`
<div id="loading-background" class="loader">
<img src="extra-textures/loading.png" id="loading-image">
<div id="loading" class="loader">
<h1 class="middle" id="loading-text">${this.loadingText}</h1>
</div>
</div>
`
}
}
window.customElements.define('loading-screen', LoadingScreen)

View file

@ -1,175 +0,0 @@
const { LitElement, html, css } = require('lit')
require('./github_link')
require('./components/button')
require('./components/buttonlink')
require('./components/textfield')
/* global fetch */
class PrismarineMenu extends LitElement {
constructor () {
super()
this.server = ''
this.serverport = 25565
this.proxy = ''
this.proxyport = ''
this.username = window.localStorage.getItem('username') ?? 'pviewer' + (Math.floor(Math.random() * 1000))
this.password = ''
fetch('config.json').then(res => res.json()).then(config => {
this.server = config.defaultHost
this.serverport = config.defaultHostPort ?? 25565
this.proxy = config.defaultProxy
this.proxyport = !config.defaultProxy && !config.defaultProxyPort ? '' : config.defaultProxyPort ?? 443
})
}
static get properties () {
return {
server: { type: String },
serverport: { type: Number },
proxy: { type: String },
proxyport: { type: Number },
username: { type: String },
password: { type: String }
}
}
static get styles () {
return css`
:host {
--guiScale: var(--guiScaleFactor, 3);
}
html {
height: 100%;
}
body {
margin:0;
padding:0;
font-family: sans-serif;
background: #000;
}
.login-box {
position: absolute;
top: 50%;
left: 50%;
width: calc(180px * var(--guiScale));
padding: calc(10px * var(--guiScale));
transform: translate(-50%, -50%);
box-sizing: border-box;
border-radius: 10px;
background: rgba(0, 0, 0, 0.5)
}
form {
display: flex;
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
flex-direction: column
}
.bottom-links {
margin-top: calc(6px * var(--guiScale));
display: flex;
flex-direction: column;
width: 100%;
}
.bottom-links span {
text-align: center;
color: rgb(175, 175, 175);
padding: calc(1px * var(--guiScale));
font-family: mojangles, minecraft, monospace;
font-size: calc(10px * var(--guiScale));
text-shadow: calc(1px * var(--guiScale)) calc(1px * var(--guiScale)) black;
}
.link-buttons {
display: flex;
justify-content: space-between;
gap: calc(4px * var(--guiScale));
}
.title, .subtitle {
text-align: center;
font-family: mojangles, minecraft, monospace;
font-size: calc(10px * var(--guiScale));
font-weight: normal;
color: white;
margin-top: 0;
text-shadow: calc(1px * var(--guiScale)) calc(1px * var(--guiScale)) black;
}
.subtitle {
font-size: calc(7.5px * var(--guiScale));
}
.wrapper {
display: flex;
justify-content: space-between;
gap: calc(6px * var(--guiScale));
}
.spacev {
height: calc(6px * var(--guiScale));
}
.field-spacev {
height: calc(14px * var(--guiScale));
}
`
}
dispatchConnect () {
window.localStorage.setItem('username', this.username)
this.dispatchEvent(new window.CustomEvent('connect', {
detail: {
server: `${this.server}:${this.serverport}`,
proxy: `${this.proxy}${this.proxy !== '' ? `:${this.proxyport}` : ''}`,
username: this.username,
password: this.password
}
}))
}
render () {
return html`
<github-link></github-link>
<div class="login-box">
<h2 class="title">Prismarine Web Client</h2>
<h3 class="subtitle" style="color: rgb(175, 175, 175)">A minecraft client in the browser!</h3>
<form>
<div class="field-spacev"></div>
<div class="wrapper">
<legacy-text-field field-width="100%" field-label="Server IP" field-id="serverip" field-value="${this.server}" @input=${e => { this.server = e.target.value }}></legacy-text-field>
<legacy-text-field field-width="100%" field-label="Server Port" field-id="port" field-value="${this.serverport}" @input=${e => { this.serverport = e.target.value }}></legacy-text-field>
</div>
<div class="field-spacev"></div>
<div class="wrapper">
<legacy-text-field field-width="100%" field-label="Proxy" field-id="proxy" field-value="${this.proxy}" @input=${e => { this.proxy = e.target.value }}></legacy-text-field>
<legacy-text-field field-width="100%" field-label="Port" field-id="port" field-value="${this.proxyport}" @input=${e => { this.proxyport = e.target.value }}></legacy-text-field>
</div>
<div class="field-spacev"></div>
<legacy-text-field field-width="100%" field-label="Username" field-id="username" field-value="${this.username}" @input=${e => { this.username = e.target.value }}></legacy-text-field>
<div class="spacev"></div>
<legacy-button btn-width="100%" @click=${() => { this.dispatchConnect() }}>Play</legacy-button>
<div class="bottom-links">
<span> Want to contribute?</span>
<div class="link-buttons">
<legacy-button-link btn-width="78px" go-to="https://github.com/PrismarineJS/prismarine-web-client">Github</legacy-button-link>
<legacy-button-link btn-width="78px" go-to="https://discord.gg/4Ucm684Fq3">Discord</legacy-button-link>
</div>
</div>
</form>
</div>
`
}
}
window.customElements.define('prismarine-menu', PrismarineMenu)

View file

@ -0,0 +1,88 @@
const { LitElement, html, css } = require('lit')
class BreathBar extends LitElement {
static get styles () {
return css`
.breathbar {
position: absolute;
display: flex;
flex-direction: row-reverse;
left: calc(50% + 91px);
transform: translate(-100%);
bottom: 40px;
--offset: calc(-1 * 16px);
--bg-x: calc(-1 * 16px);
--bg-y: calc(-1 * 18px);
}
.breath {
width: 9px;
height: 9px;
margin-left: -1px;
}
.breath.full {
background-image: url('textures/1.17.1/gui/icons.png');
background-size: 256px;
background-position: var(--offset) var(--bg-y);
}
.breath.half {
background-image: url('textures/1.17.1/gui/icons.png');
background-size: 256px;
background-position: calc(var(--offset) - 9) var(--bg-y);
}
`
}
gameModeChanged (gamemode) {
this.shadowRoot.querySelector('#breathbar').classList.toggle('creative', gamemode === 1)
}
updateOxygen (hValue) {
const breathbar = this.shadowRoot.querySelector('#breathbar')
breathbar.style.display = 'block'
const breaths = breathbar.children
for (let i = 0; i < breaths.length; i++) {
breaths[i].classList.remove('full')
breaths[i].classList.remove('half')
}
for (let i = 0; i < Math.ceil(hValue / 2); i++) {
if (i >= breaths.length) break
if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) {
breaths[i].classList.add('half')
} else {
breaths[i].classList.add('full')
}
}
// if (hValue === 20) {
// setTimeout(() => {
// breathbar.style.display = 'none'
// }, 1000)
// }
}
render () {
return html`
<div id="breathbar" class="breathbar">
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
<div class="breath"></div>
</div>
`
}
}
window.customElements.define('pmui-breathbar', BreathBar)

View file

@ -0,0 +1,140 @@
const { LitElement, html, css } = require('lit')
const audioContext = new window.AudioContext()
const sounds = {}
async function playSound (path) {
let volume = 1
const options = document.getElementById('options-screen')
if (options) {
volume = options.sound / 100
}
let soundBuffer = sounds[path]
if (!soundBuffer) {
const res = await window.fetch(path)
const data = await res.arrayBuffer()
soundBuffer = await audioContext.decodeAudioData(data)
sounds[path] = soundBuffer
}
const gainNode = audioContext.createGain()
const source = audioContext.createBufferSource()
source.buffer = soundBuffer
source.connect(gainNode)
gainNode.connect(audioContext.destination)
gainNode.gain.value = volume
source.start(0)
}
class Button extends LitElement {
static get styles () {
return css`
.button {
--txrV: 66px;
position: relative;
width: 200px;
height: 20px;
font-family: minecraft, mojangles, monospace;
font-size: 10px;
color: white;
text-shadow: 1px 1px #222;
border: none;
z-index: 1;
outline: none;
}
.button:hover,
.button:focus-visible {
--txrV: 86px;
}
.button:disabled {
--txrV: 46px;
color: #A0A0A0;
text-shadow: 1px 1px #111;
}
.button::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 20px;
background: url('textures/1.17.1/gui/widgets.png');
background-size: 256px;
background-position-y: calc(var(--txrV) * -1);
z-index: -1;
}
.button::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 20px;
background: url('textures/1.17.1/gui/widgets.png');
background-size: 256px;
background-position-x: calc(-200px + 100%);
background-position-y: calc(var(--txrV) * -1);
z-index: -1;
}
`
}
static get properties () {
return {
label: {
type: String,
attribute: 'pmui-label'
},
width: {
type: String,
attribute: 'pmui-width'
},
disabled: {
type: Boolean,
attribute: 'pmui-disabled'
},
onPress: {
type: Function,
attribute: 'pmui-click'
}
}
}
constructor () {
super()
this.label = ''
this.disabled = false
this.width = '200px'
this.onPress = () => {}
}
render () {
return html`
<button
class="button"
?disabled=${this.disabled}
@click=${this.onBtnClick}
style="width: ${this.width};"
>
${this.label}
</button>`
}
onBtnClick () {
playSound('click_stereo.ogg')
this.dispatchEvent(new window.CustomEvent('pmui-click'))
}
}
window.customElements.define('pmui-button', Button)
const _playSound = playSound
export { _playSound as playSound }

View file

@ -0,0 +1,65 @@
import { css } from 'lit'
const commonCss = css`
.dirt-bg {
position: absolute;
top: 0;
left: 0;
background: url('textures/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.75);
background-size: 16px;
background-repeat: repeat;
width: 100%;
height: 100%;
transform-origin: top left;
transform: scale(2);
background-blend-mode: overlay;
}
.bg {
position: absolute;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.75);
width: 100%;
height: 100%;
}
.title {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%);
font-size: 10px;
color: white;
text-align: center;
text-shadow: 1px 1px #222;
}
.text {
color: white;
font-size: 10px;
text-shadow: 1px 1px #222;
}
`
/**
* @param {string} url
*/
function openURL (url) {
window.open(url, '_blank', 'noopener,noreferrer')
}
/**
* @param {HTMLElement} prev
* @param {HTMLElement} next
*/
function displayScreen (prev, next) {
prev.style.display = 'none'
next.style.display = 'block'
}
export {
commonCss,
openURL,
displayScreen
}

View file

@ -0,0 +1,146 @@
const { LitElement, html, css } = require('lit')
class DebugOverlay extends LitElement {
static get styles () {
return css`
.debug-left-side,
.debug-right-side {
position: absolute;
display: flex;
flex-direction: column;
z-index: 40;
}
.debug-left-side {
top: 1px;
left: 1px;
}
.debug-right-side {
top: 1px;
right: 1px;
}
p {
display: block;
color: white;
font-size: 10px;
width: fit-content;
height: 9px;
margin: 0;
padding: 0;
padding-bottom: 1px;
background: rgba(110, 110, 110, 0.5);
}
.debug-right-side p {
margin-left: auto;
}
.empty {
display: block;
height: 9px;
}
`
}
static get properties () {
return {
showOverlay: { type: Boolean },
cursorBlock: { type: Object },
bot: { type: Object },
customEntries: { type: Object }
}
}
constructor () {
super()
this.showOverlay = false
this.customEntries = {}
}
firstUpdated () {
document.addEventListener('keydown', e => {
e ??= window.event
if (e.key === 'F3') {
this.showOverlay = !this.showOverlay
e.preventDefault()
}
})
}
updated (changedProperties) {
if (changedProperties.has('bot')) {
this.bot.on('move', () => {
this.requestUpdate()
})
this.bot.on('time', () => {
this.requestUpdate()
})
this.bot.on('entitySpawn', () => {
this.requestUpdate()
})
this.bot.on('entityGone', () => {
this.requestUpdate()
})
}
}
render () {
if (!this.showOverlay) {
return html``
}
const target = this.cursorBlock
const targetDiggable = (target && this.bot.canDigBlock(target))
const pos = this.bot.entity.position
const rot = [this.bot.entity.yaw, this.bot.entity.pitch]
const viewDegToMinecraft = (yaw) => yaw % 360 - 180 * (yaw < 0 ? -1 : 1)
const quadsDescription = [
'north (towards negative Z)',
'east (towards positive X)',
'south (towards positive Z)',
'west (towards negative X)'
]
const minecraftYaw = viewDegToMinecraft(rot[0] * -180 / Math.PI)
const minecraftQuad = Math.floor(((minecraftYaw + 180) / 90 + 0.5) % 4)
const renderProp = (name, value) => {
return html`<p>${name}: ${typeof value === 'boolean' ? html`<span style="color: ${value ? 'lightgreen' : 'red'}">${value}</span>` : value}</p>`
}
const skyL = this.bot.world.getSkyLight(this.bot.entity.position)
const biomeId = this.bot.world.getBiome(this.bot.entity.position)
return html`
<div class="debug-left-side">
<p>Prismarine Web Client (${this.bot.version})</p>
<p>E: ${Object.values(this.bot.entities).length}</p>
<p>${this.bot.game.dimension}</p>
<div class="empty"></div>
<p>XYZ: ${pos.x.toFixed(3)} / ${pos.y.toFixed(3)} / ${pos.z.toFixed(3)}</p>
<p>Chunk: ${Math.floor(pos.x % 16)} ~ ${Math.floor(pos.z % 16)} in ${Math.floor(pos.x / 16)} ~ ${Math.floor(pos.z / 16)}</p>
<p>Facing (viewer): ${rot[0].toFixed(3)} ${rot[1].toFixed(3)}</p>
<p>Facing (minecraft): ${quadsDescription[minecraftQuad]} (${minecraftYaw.toFixed(1)} ${(rot[1] * -180 / Math.PI).toFixed(1)})</p>
<p>Light: ${skyL} (${skyL} sky)</p>
<p>Biome: minecraft:${window.mcData.biomesArray[biomeId].name}</p>
<p>Day: ${this.bot.time.day}</p>
<div class="empty"></div>
${Object.entries(this.customEntries).map(([name, value]) => html`<p>${name}: ${value}</p>`)}
</div>
<div class="debug-right-side">
<p>Renderer: three.js r${global.THREE.REVISION}</p>
<div class="empty"></div>
${targetDiggable ? html`<p>${target.name}</p>${Object.entries(target.getProperties()).map(([n, p], idx, arr) => renderProp(n, p, arr[idx + 1]))}` : ''}
${targetDiggable ? html`<p>Looking at: ${target.position.x} ${target.position.y} ${target.position.z}</p>` : ''}
</div>
`
}
}
window.customElements.define('pmui-debug-overlay', DebugOverlay)

View file

@ -0,0 +1,97 @@
const { LitElement, html, css } = require('lit')
class EditBox extends LitElement {
static get styles () {
return css`
.edit-container {
position: relative;
width: 200px;
height: 20px;
background: black;
border: 1px solid grey;
}
.edit-container:hover,
.edit-container:focus-within {
border-color: white;
}
.edit-container label {
position: absolute;
z-index: 2;
pointer-events: none;
bottom: 21px;
left: 0;
font-size: 10px;
color: rgb(206, 206, 206);
text-shadow: 1px 1px black;
}
.edit-box {
position: relative;
outline: none;
border: none;
background: none;
left: 1px;
width: calc(100% - 2px);
height: 100%;
font-family: minecraft, mojangles, monospace;
font-size: 10px;
color: white;
text-shadow: 1px 1px #222;
}
`
}
constructor () {
super()
this.width = '200px'
this.id = ''
this.value = ''
this.label = ''
}
static get properties () {
return {
width: {
type: String,
attribute: 'pmui-width'
},
id: {
type: String,
attribute: 'pmui-id'
},
label: {
type: String,
attribute: 'pmui-label'
},
value: {
type: String,
attribute: 'pmui-value'
}
}
}
render () {
return html`
<div
class="edit-container"
style="width: ${this.width};"
>
<label for="${this.id}">${this.label}</label>
<input
id="${this.id}"
type="text"
name=""
spellcheck="false"
required=""
autocomplete="off"
value="${this.value}"
@input=${(e) => { this.value = e.target.value }}
class="edit-box">
</div>
`
}
}
window.customElements.define('pmui-editbox', EditBox)

View file

@ -0,0 +1,117 @@
const { LitElement, html, css } = require('lit')
class FoodBar extends LitElement {
static get styles () {
return css`
.foodbar {
position: absolute;
display: flex;
flex-direction: row-reverse;
left: calc(50% + 91px);
transform: translate(-100%);
bottom: 30px;
--lightened: 0;
--offset: calc(-1 * (52px));
--bg-x: calc(-1 * (16px + 9px * var(--lightened)));
--bg-y: calc(-1 * 27px);
}
.food {
width: 9px;
height: 9px;
background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png');
background-size: 256px, 256px;
background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y);
margin-left: -1px;
}
.food.full {
background-position: var(--offset) var(--bg-y), var(--bg-x) var(--bg-y);
}
.food.half {
background-position: calc(var(--offset) - 9px) var(--bg-y), var(--bg-x) var(--bg-y);
}
.foodbar.low .food {
animation: lowHungerAnim 0.2s steps(2, end) infinite;
}
.foodbar.low .food:nth-of-type(2n) {
animation-direction: reverse;
}
.foodbar.low .food:nth-of-type(3n) {
animation-duration: 0.1s;
}
.foodbar.updated {
animation: updatedAnim 0.3s steps(2, end) 2;
}
@keyframes lowHungerAnim {
to { transform: translateY(1px); }
}
@keyframes updatedAnim {
to { --lightened: 1; }
}
`
}
gameModeChanged (gamemode) {
this.shadowRoot.querySelector('#foodbar').classList.toggle('creative', gamemode === 1)
}
onHungerUpdate () {
this.shadowRoot.querySelector('#foodbar').classList.toggle('updated', true)
if (this.hungerTimeout) clearTimeout(this.hungerTimeout)
this.hungerTimeout = setTimeout(() => {
this.shadowRoot.querySelector('#foodbar').classList.toggle('updated', false)
this.hungerTimeout = null
}, 1000)
}
updateHunger (hValue, d) {
const foodbar = this.shadowRoot.querySelector('#foodbar')
foodbar.classList.toggle('low', hValue <= 5)
const foods = foodbar.children
for (let i = 0; i < foods.length; i++) {
foods[i].classList.remove('full')
foods[i].classList.remove('half')
}
// if (d) this.onHungerUpdate()
for (let i = 0; i < Math.ceil(hValue / 2); i++) {
if (i >= foods.length) break
if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) {
foods[i].classList.add('half')
} else {
foods[i].classList.add('full')
}
}
}
render () {
return html`
<div id="foodbar" class="foodbar" data-value="4">
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
<div class="food"></div>
</div>
`
}
}
window.customElements.define('pmui-foodbar', FoodBar)

View file

@ -0,0 +1,158 @@
const { LitElement, html, css } = require('lit')
function getEffectClass (effect) {
switch (effect.id) {
case 19: return 'poisoned'
case 20: return 'withered'
case 22: return 'absorption'
default: return ''
}
}
class HealthBar extends LitElement {
static get styles () {
return css`
.health {
position: absolute;
display: flex;
flex-direction: row;
left: calc(50% - 91px);
bottom: 30px;
--hardcore: 0;
--kind: 0;
--lightened: 0;
--offset: calc(-1 * (52px + (9px * (4 * var(--kind) + var(--lightened) * 2)) ));
--bg-x: calc(-1 * (16px + 9px * var(--lightened)));
--bg-y: calc(-1 * var(--hardcore) * 45px);
}
.health.creative {
display: none;
}
.health.hardcore {
--hardcore: 1;
}
.health.poisoned {
--kind: 1;
}
.health.withered {
--kind: 2;
}
.health.absorption {
--kind: 3;
}
.heart {
width: 9px;
height: 9px;
background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png');
background-size: 256px, 256px;
background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y);
margin-left: -1px;
}
.heart.full {
background-position: var(--offset) var(--bg-y), var(--bg-x) var(--bg-y);
}
.heart.half {
background-position: calc(var(--offset) - 9px) var(--bg-y), var(--bg-x) var(--bg-y);
}
.health.low .heart {
animation: lowHealthAnim 0.2s steps(2, end) infinite;
}
.health.low .heart:nth-of-type(2n) {
animation-direction: reverse;
}
.health.low .heart:nth-of-type(3n) {
animation-duration: 0.1s;
}
.health.damaged {
animation: damagedAnim 0.3s steps(2, end) 2;
}
@keyframes lowHealthAnim {
to {
transform: translateY(1px);
}
}
@keyframes damagedAnim {
to { --lightened: 1; }
}
`
}
effectAdded (effect) {
this.shadowRoot.querySelector('#health').classList.add(getEffectClass(effect))
}
effectEnded (effect) {
this.shadowRoot.querySelector('#health').classList.remove(getEffectClass(effect))
}
onDamage () {
this.shadowRoot.querySelector('#health').classList.toggle('damaged', true)
if (this.hurtTimeout) clearTimeout(this.hurtTimeout)
this.hurtTimeout = setTimeout(() => {
this.shadowRoot.querySelector('#health').classList.toggle('damaged', false)
this.hurtTimeout = null
}, 1000)
}
gameModeChanged (gamemode, hardcore) {
this.shadowRoot.querySelector('#health').classList.toggle('creative', gamemode === 1)
this.shadowRoot.querySelector('#health').classList.toggle('hardcore', hardcore)
}
updateHealth (hValue, d) {
const health = this.shadowRoot.querySelector('#health')
health.classList.toggle('low', hValue <= 4)
const hearts = health.children
for (let i = 0; i < hearts.length; i++) {
hearts[i].classList.remove('full')
hearts[i].classList.remove('half')
}
if (d) this.onDamage()
for (let i = 0; i < Math.ceil(hValue / 2); i++) {
if (i >= hearts.length) break
if (hValue % 2 !== 0 && Math.ceil(hValue / 2) === i + 1) {
hearts[i].classList.add('half')
} else {
hearts[i].classList.add('full')
}
}
}
render () {
return html`
<div id="health" class="health">
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
<div class="heart"></div>
</div>
`
}
}
window.customElements.define('pmui-healthbar', HealthBar)

View file

@ -0,0 +1,210 @@
const { LitElement, html, css } = require('lit')
const invsprite = require('../../invsprite.json')
class Hotbar extends LitElement {
static get styles () {
return css`
.hotbar {
position: absolute;
bottom: 0;
left: 50%;
transform: translate(-50%);
width: 182px;
height: 22px;
background: url("textures/1.16.4/gui/widgets.png");
background-size: 256px;
}
#hotbar-selected {
position: absolute;
left: -1px;
top: -1px;
width: 24px;
height: 24px;
background: url("textures/1.16.4/gui/widgets.png");
background-size: 256px;
background-position-y: -22px;
}
#hotbar-items-wrapper {
position: absolute;
top: 0;
left: 1px;
display: flex;
flex-direction: row;
height: 22px;
margin: 0;
padding: 0;
}
.hotbar-item {
position: relative;
width: 20px;
height: 22px;
}
.item-icon {
top: 3px;
left: 2px;
position: absolute;
width: 32px;
height: 32px;
transform-origin: top left;
transform: scale(0.5);
background-image: url('invsprite.png');
background-size: 1024px auto;
}
.item-stack {
position: absolute;
color: white;
font-size: 10px;
text-shadow: 1px 1px 0 rgb(63, 63, 63);
right: 1px;
bottom: 1px;
}
#hotbar-item-name {
color: white;
position: absolute;
bottom: 51px;
left: 50%;
transform: translate(-50%);
text-shadow: rgb(63, 63, 63) 1px 1px 0px;
font-family: mojangles, minecraft, monospace;
font-size: 10px;
text-align: center;
}
.hotbar-item-name-fader {
opacity: 0;
transition: visibility 0s, opacity 1s linear;
transition-delay: 2s;
}
`
}
static get properties () {
return {
activeItemName: { type: String },
bot: { type: Object },
viewerVersion: { type: String }
}
}
constructor () {
super()
this.activeItemName = ''
}
updated (changedProperties) {
if (changedProperties.has('bot')) {
// inventory listener
this.bot.once('spawn', () => {
this.init()
})
}
}
init () {
this.reloadHotbar()
this.reloadHotbarSelected(0)
document.addEventListener('wheel', (e) => {
const newSlot = ((this.bot.quickBarSlot + Math.sign(e.deltaY)) % 9 + 9) % 9
this.reloadHotbarSelected(newSlot)
})
document.addEventListener('keydown', (e) => {
const numPressed = e.code.substr(5)
if (numPressed < 1 || numPressed > 9) return
this.reloadHotbarSelected(numPressed - 1)
})
this.bot.inventory.on('updateSlot', (slot, oldItem, newItem) => {
if (slot >= this.bot.inventory.hotbarStart + 9) return
if (slot < this.bot.inventory.hotbarStart) return
const sprite = newItem ? invsprite[newItem.name] : invsprite.air
const slotEl = this.shadowRoot.getElementById('hotbar-' + (slot - this.bot.inventory.hotbarStart))
const slotIcon = slotEl.children[0]
const slotStack = slotEl.children[1]
slotIcon.style['background-position-x'] = `-${sprite.x}px`
slotIcon.style['background-position-y'] = `-${sprite.y}px`
slotStack.innerHTML = newItem?.count > 1 ? newItem.count : ''
})
}
async reloadHotbar () {
for (let i = 0; i < 9; i++) {
const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + i]
const sprite = item ? invsprite[item.name] : invsprite.air
const slotEl = this.shadowRoot.getElementById('hotbar-' + i)
const slotIcon = slotEl.children[0]
const slotStack = slotEl.children[1]
slotIcon.style['background-position-x'] = `-${sprite.x}px`
slotIcon.style['background-position-y'] = `-${sprite.y}px`
slotStack.innerHTML = item?.count > 1 ? item.count : ''
}
}
async reloadHotbarSelected (slot) {
const item = this.bot.inventory.slots[this.bot.inventory.hotbarStart + slot]
const newLeftPos = (-1 + 20 * slot) + 'px'
this.shadowRoot.getElementById('hotbar-selected').style.left = newLeftPos
this.bot.setQuickBarSlot(slot)
this.activeItemName = item?.displayName ?? ''
const name = this.shadowRoot.getElementById('hotbar-item-name')
name.classList.remove('hotbar-item-name-fader')
setTimeout(() => name.classList.add('hotbar-item-name-fader'), 10)
}
render () {
return html`
<div class="hotbar">
<p id="hotbar-item-name">${this.activeItemName}</p>
<div id="hotbar-selected"></div>
<div id="hotbar-items-wrapper">
<div class="hotbar-item" id="hotbar-0">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-1">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-2">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-3">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-4">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-5">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-6">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-7">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-8">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
</div>
</div>
`
}
}
window.customElements.define('pmui-hotbar', Hotbar)

View file

@ -0,0 +1,179 @@
const { LitElement, html, css } = require('lit')
const MAX_ROWS_PER_COL = 10
class PlayerListOverlay extends LitElement {
static get styles () {
return css`
.playerlist-container {
position: absolute;
background-color: rgba(0, 0, 0, 0.3);
top: 9px;
left: 50%;
transform: translate(-50%);
width: fit-content;
padding: 1px;
display: flex;
flex-direction: column;
gap: 1px 0;
place-items: center;
z-index: 30;
}
.title {
color: white;
text-shadow: 1px 1px 0px #3f3f3f;
font-size: 10px;
margin: 0;
padding: 0;
}
.playerlist-entry {
overflow: hidden;
color: white;
font-size: 10px;
margin: 0px;
line-height: calc(100% - 1px);
text-shadow: 1px 1px 0px #3f3f3f;
font-family: mojangles, minecraft, monospace;
background: rgba(255, 255, 255, 0.1);
width: 100%;
}
.active-player {
color: rgb(42, 204, 237);
text-shadow: 1px 1px 0px rgb(4, 44, 67);
}
.playerlist-ping {
text-align: right;
float: right;
padding-left: 10px;
}
.playerlist-ping-value {
color: rgb(114, 255, 114);
text-shadow: 1px 1px 0px rgb(28, 105, 28);
float: left;
margin: 0;
margin-right: 1px;
}
.playerlist-ping-label {
text-shadow: 1px 1px 0px #3f3f3f;
color: white;
float: right;
margin: 0px;
}
.player-lists {
display: flex;
flex-direction: row;
place-items: center;
place-content: center;
gap: 0 4px;
}
.player-list {
display: flex;
flex-direction: column;
gap: 1px 0;
min-width: 80px;
}
`
}
static get properties () {
return {
serverIP: { type: String },
clientId: { type: String },
players: { type: Object }
}
}
constructor () {
super()
this.serverIP = ''
this.clientId = ''
this.players = {}
}
init (bot, ip) {
const playerList = this.shadowRoot.querySelector('#playerlist-container')
this.isOpen = false
this.players = bot.players
this.clientId = bot.player.uuid
this.serverIP = ip
this.requestUpdate()
const showList = (shouldShow = true) => {
playerList.style.display = shouldShow ? 'block' : 'none'
this.isOpen = shouldShow
}
document.addEventListener('keydown', e => {
e ??= window.event
if (e.key === 'Tab') {
showList(true)
e.preventDefault()
}
})
document.addEventListener('keyup', e => {
if (!this.isOpen) return
e ??= window.event
if (e.key === 'Tab') {
showList(false)
e.preventDefault()
}
})
bot.on('playerUpdated', () => this.requestUpdate()) // LitElement seems to be batching requests, so it should be fine?
bot.on('playerJoined', () => this.requestUpdate())
bot.on('playerLeft', () => this.requestUpdate())
}
render () {
const lists = []
const players = Object.values(this.players).sort((a, b) => {
if (a.username > b.username) return 1
if (a.username < b.username) return -1
return 0
})
let tempList = []
for (let i = 0; i < players.length; i++) {
tempList.push(players[i])
if ((i + 1) / MAX_ROWS_PER_COL === 1 || i + 1 === players.length) {
lists.push([...tempList])
tempList = []
}
}
return html`
<div class="playerlist-container" id="playerlist-container" style="display: none;">
<span class="title">Server IP: ${this.serverIP}</span>
<div class="player-lists">
${lists.map(list => html`
<div class="player-list">
${list.map(player => html`
<div class="playerlist-entry${this.clientId === player.uuid ? ' active-player' : ''}" id="plist-player-${player.uuid}">
${player.username}
<div class="playerlist-ping">
<p class="playerlist-ping-value">${player.ping}</p>
<p class="playerlist-ping-label">ms</p>
</div>
</div>
`)}
</div>
`)}
</div>
</div>
`
}
}
window.customElements.define('pmui-playerlist-overlay', PlayerListOverlay)

View file

@ -0,0 +1,176 @@
const { LitElement, html, css } = require('lit')
class Slider extends LitElement {
static get styles () {
return css`
.slider-container {
--txrV: -46px;
position: relative;
width: 150px;
height: 20px;
font-family: minecraft, mojangles, monospace;
font-size: 10px;
color: white;
text-shadow: 1px 1px #220;
z-index: 1;
}
.slider-thumb {
--txrV: -66px;
pointer-events: none;
width: 8px;
height: 20px;
position: absolute;
top: 0;
left: 0;
z-index: 3;
}
.slider-container:hover .slider-thumb {
--txrV: -86px;
}
.slider-container::after,
.slider-thumb::after {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 20px;
background: url('textures/1.17.1/gui/widgets.png');
background-size: 256px;
background-position-y: var(--txrV);
z-index: -1;
}
.slider-container::before,
.slider-thumb::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 20px;
background: url('textures/1.17.1/gui/widgets.png');
background-size: 256px;
background-position-x: calc(-200px + 100%);
background-position-y: var(--txrV);
z-index: -1;
}
.slider {
display: block;
position: absolute;
top: 0;
left: 0;
-webkit-appearance: none;
appearance: none;
background: none;
width: 100%;
height: 20px;
margin: 0;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
position: relative;
appearance: none;
width: 8px;
height: 20px;
background: transparent;
}
.slider::-moz-range-thumb {
width: 8px;
height: 20px;
background: transparent;
}
label {
pointer-events: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 6;
width: max-content;
}
`
}
constructor () {
super()
this.label = ''
this.type = '%'
this.width = '150px'
this.value = '50'
this.min = '0'
this.max = '100'
this.ratio = (Number(this.value) - Number(this.min)) / (Number(this.max) - Number(this.min))
}
updated () {
this.ratio = (Number(this.value) - Number(this.min)) / (Number(this.max) - Number(this.min))
}
static get properties () {
return {
label: {
type: String,
attribute: 'pmui-label'
},
type: {
type: String,
attribute: 'pmui-type'
},
width: {
type: String,
attribute: 'pmui-width'
},
value: {
type: String,
attribute: 'pmui-value'
},
min: {
type: String,
attribute: 'pmui-min'
},
max: {
type: String,
attribute: 'pmui-max'
},
ratio: { type: Number }
}
}
render () {
return html`
<div
class="slider-container"
style="width: ${this.width};"
>
<input
type="range"
class="slider"
min="${this.min}"
max="${this.max}"
value="${this.value}"
@input=${(e) => {
const range = e.target
this.ratio = (range.value - range.min) / (range.max - range.min)
this.value = range.value
}}>
<div
class="slider-thumb"
style="left: calc((100% * ${this.ratio}) - (8px * ${this.ratio}));"
></div>
<label>${this.label}: ${this.value}${this.type}</label>
</div>
`
}
}
window.customElements.define('pmui-slider', Slider)

148
lib/menus/hud.js Normal file
View file

@ -0,0 +1,148 @@
const { LitElement, html, css } = require('lit')
class Hud extends LitElement {
static get styles () {
return css`
:host {
position: absolute;
top: 0;
left: 0;
z-index: -2;
width: 100%;
height: 100%;
}
.crosshair {
width: 16px;
height: 16px;
background: url('textures/1.17.1/gui/icons.png');
background-size: 256px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
}
#xp-label {
position: absolute;
top: -8px;
left: 50%;
transform: translate(-50%);
font-size: 10px;
font-family: minecraft, mojangles, monospace;
color: rgb(30, 250, 30);
text-shadow: 0px -1px #000, 0px 1px #000, 1px 0px #000, -1px 0px #000;
z-index: 10;
}
#xp-bar-bg {
position: absolute;
left: 50%;
bottom: 24px;
transform: translate(-50%);
width: 182px;
height: 5px;
background-image: url('textures/1.16.4/gui/icons.png');
background-size: 256px;
background-position-y: -64px;
}
.xp-bar {
width: 182px;
height: 5px;
background-image: url('textures/1.17.1/gui/icons.png');
background-size: 256px;
background-position-y: -69px;
}
`
}
/**
* @param {globalThis.THREE.Renderer} renderer
* @param {import('mineflayer').Bot} bot
* @param {string} host
*/
init (renderer, bot, host) {
const debugMenu = this.shadowRoot.querySelector('#debug-overlay')
const playerList = this.shadowRoot.querySelector('#playerlist-overlay')
const healthbar = this.shadowRoot.querySelector('#health-bar')
const foodbar = this.shadowRoot.querySelector('#food-bar')
// const breathbar = this.shadowRoot.querySelector('#breath-bar')
const chat = this.shadowRoot.querySelector('#chat')
const hotbar = this.shadowRoot.querySelector('#hotbar')
const xpLabel = this.shadowRoot.querySelector('#xp-label')
hotbar.bot = bot
debugMenu.bot = bot
chat.init(bot._client, renderer)
playerList.init(bot, host)
bot.on('entityHurt', (entity) => {
if (entity !== bot.entity) return
healthbar.onDamage()
})
bot.on('entityEffect', (entity, effect) => {
if (entity !== bot.entity) return
healthbar.effectAdded(effect)
})
bot.on('entityEffectEnd', (entity, effect) => {
if (entity !== bot.entity) return
healthbar.effectEnded(effect)
})
bot.on('game', () => {
healthbar.gameModeChanged(bot.player.gamemode, bot.game.hardcore)
foodbar.gameModeChanged(bot.player.gamemode)
// breathbar.gameModeChanged(bot.player.gamemode)
this.shadowRoot.querySelector('#xp-bar-bg').style.display = bot.player.gamemode === 1 ? 'none' : 'block'
})
bot.on('health', () => {
healthbar.updateHealth(bot.health, true)
foodbar.updateHunger(bot.food, true)
})
bot.on('experience', () => {
this.shadowRoot.querySelector('#xp-bar-bg').firstElementChild.style.width = `${182 * bot.experience.progress}px`
xpLabel.innerHTML = bot.experience.level
xpLabel.style.display = bot.experience.level > 0 ? 'block' : 'none'
})
// bot.on('breath', () => {
// breathbar.updateOxygen(bot.oxygenLevel)
// })
this.shadowRoot.querySelector('#xp-bar-bg').style.display = bot.player.gamemode === 1 ? 'none' : 'block'
this.shadowRoot.querySelector('#xp-bar-bg').firstElementChild.style.width = `${182 * bot.experience.progress}px`
xpLabel.innerHTML = bot.experience.level
xpLabel.style.display = bot.experience.level > 0 ? 'block' : 'none'
healthbar.gameModeChanged(bot.player.gamemode, bot.game.hardcore)
healthbar.updateHealth(bot.health)
foodbar.updateHunger(bot.food)
// breathbar.updateOxygen(bot.oxygenLevel ?? 20)
hotbar.init()
}
render () {
return html`
<pmui-debug-overlay id="debug-overlay"></pmui-debug-overlay>
<pmui-playerlist-overlay id="playerlist-overlay"></pmui-playerlist-overlay>
<div class="crosshair"></div>
<chat-box id="chat"></chat-box>
<!--<pmui-breathbar id="breath-bar"></pmui-breathbar>-->
<pmui-healthbar id="health-bar"></pmui-healthbar>
<pmui-foodbar id="food-bar"></pmui-foodbar>
<div id="xp-bar-bg">
<div class="xp-bar"></div>
<span id="xp-label"></span>
</div>
<pmui-hotbar id="hotbar"></pmui-hotbar>
`
}
}
window.customElements.define('pmui-hud', Hud)

View file

@ -0,0 +1,172 @@
const { LitElement, html, css } = require('lit')
const { commonCss, displayScreen } = require('./components/common')
class KeyBindsScreen extends LitElement {
static get styles () {
return css`
${commonCss}
.title {
top: 4px;
}
main {
display: flex;
flex-direction: column;
position: absolute;
top: 30px;
left: 50%;
transform: translate(-50%);
width: 100%;
height: calc(100% - 64px);
place-items: center;
background: rgba(0, 0, 0, 0.5);
box-shadow: inset 0 3px 6px rgba(0, 0, 0, 0.7), inset 0 -3px 6px rgba(0, 0, 0, 0.7);
}
.keymap-list {
width: 288px;
display: flex;
flex-direction: column;
padding: 4px 0;
overflow-y: auto;
}
.keymap-list::-webkit-scrollbar {
width: 6px;
}
.keymap-list::-webkit-scrollbar-track {
background: #000;
}
.keymap-list::-webkit-scrollbar-thumb {
background: #ccc;
box-shadow: inset -1px -1px 0 #4f4f4f;
}
.keymap-entry {
display: flex;
flex-direction: row;
width: 100%;
height: 20px;
place-content: center;
place-items: center;
justify-content: space-between;
}
span {
color: white;
text-shadow: 1px 1px 0 rgb(63, 63, 63);
font-size: 10px;
}
.keymap-entry-btns {
display: flex;
flex-direction: row;
gap: 4px;
}
.bottom-btns {
display: flex;
flex-direction: row;
width: 310px;
height: 20px;
justify-content: space-between;
position: absolute;
bottom: 9px;
left: 50%;
transform: translate(-50%);
}
`
}
static get properties () {
return {
keymaps: { type: Object },
selected: { type: Number }
}
}
constructor () {
super()
this.selected = -1
this.keymaps = [
{ defaultKey: 'KeyW', key: 'KeyW', name: 'Walk Forwards' },
{ defaultKey: 'KeyS', key: 'KeyS', name: 'Walk Backwards' },
{ defaultKey: 'KeyA', key: 'KeyA', name: 'Strafe Left' },
{ defaultKey: 'KeyD', key: 'KeyD', name: 'Strafe Right' },
{ defaultKey: 'Space', key: 'Space', name: 'Jump' },
{ defaultKey: 'ShiftLeft', key: 'ShiftLeft', name: 'Sneak' },
{ defaultKey: 'ControlLeft', key: 'ControlLeft', name: 'Sprint' },
{ defaultKey: 'KeyT', key: 'KeyT', name: 'Open Chat' },
{ defaultKey: 'Slash', key: 'Slash', name: 'Open Command' },
// { defaultKey: '0', key: '0', name: 'Attack/Destroy' },
// { defaultKey: '1', key: '1', name: 'Place Block' },
{ defaultKey: 'KeyQ', key: 'KeyQ', name: 'Drop Item' }
// { defaultKey: 'Digit1', key: 'Digit1', name: 'Hotbar Slot 1' },
// { defaultKey: 'Digit2', key: 'Digit2', name: 'Hotbar Slot 2' },
// { defaultKey: 'Digit3', key: 'Digit3', name: 'Hotbar Slot 3' },
// { defaultKey: 'Digit4', key: 'Digit4', name: 'Hotbar Slot 4' },
// { defaultKey: 'Digit5', key: 'Digit5', name: 'Hotbar Slot 5' },
// { defaultKey: 'Digit6', key: 'Digit6', name: 'Hotbar Slot 6' },
// { defaultKey: 'Digit7', key: 'Digit7', name: 'Hotbar Slot 7' },
// { defaultKey: 'Digit8', key: 'Digit8', name: 'Hotbar Slot 8' },
// { defaultKey: 'Digit9', key: 'Digit9', name: 'Hotbar Slot 9' },
// { defaultKey: 'KeyE', key: 'KeyE', name: 'Open Inventory' }
]
document.addEventListener('keydown', (e) => {
if (this.selected !== -1) {
this.keymaps[this.selected].key = e.code
this.selected = -1
this.requestUpdate()
}
})
}
render () {
return html`
<div class="dirt-bg"></div>
<p class="title">Key Binds</p>
<main>
<div class="keymap-list">
${this.keymaps.map((m, i) => html`
<div class="keymap-entry">
<span>${m.name}</span>
<div class="keymap-entry-btns">
<pmui-button pmui-width="72px" pmui-label="${this.selected === i ? `> ${m.key} <` : m.key}" @pmui-click=${e => {
e.target.setAttribute('pmui-label', `> ${m.key} <`)
this.selected = i
this.requestUpdate()
}}></pmui-button>
<pmui-button pmui-width="50px" ?pmui-disabled=${m.key === m.defaultKey} pmui-label="Reset" @pmui-click=${() => {
this.keymaps[i].key = this.keymaps[i].defaultKey
this.requestUpdate()
this.selected = -1
}}></pmui-button>
</div>
</div>
`)}
</div>
</main>
<div class="bottom-btns">
<pmui-button pmui-width="150px" pmui-label="Reset All Keys" ?pmui-disabled=${!this.keymaps.some(v => v.key !== v.defaultKey)} @pmui-click=${this.onResetAllPress}></pmui-button>
<pmui-button pmui-width="150px" pmui-label="Done" @pmui-click=${() => displayScreen(this, document.getElementById('options-screen'))}></pmui-button>
</div>
`
}
onResetAllPress () {
for (let i = 0; i < this.keymaps.length; i++) {
this.keymaps[i].key = this.keymaps[i].defaultKey
}
this.requestUpdate()
}
}
window.customElements.define('pmui-keybindsscreen', KeyBindsScreen)

View file

@ -0,0 +1,67 @@
const { LitElement, html, css } = require('lit')
const { commonCss } = require('./components/common')
class LoadingScreen extends LitElement {
static get styles () {
return css`
${commonCss}
.title {
top: 30px;
}
#cancel-btn {
position: absolute;
top: calc(20% + 50px);
left: 50%;
transform: translate(-50%);
}
`
}
static get properties () {
return {
status: { type: String },
loadingText: { type: String },
hasError: { type: Number }
}
}
constructor () {
super()
this.hasError = false
this.status = 'Waiting for JS load'
}
firstUpdated () {
this.statusRunner()
}
async statusRunner () {
const array = ['.', '..', '...', '']
const timer = ms => new Promise((resolve) => setTimeout(resolve, ms))
const load = async () => {
for (let i = 0; true; i = ((i + 1) % array.length)) {
this.loadingText = this.status + array[i]
await timer(500)
}
}
load()
}
render () {
return html`
<div class="dirt-bg"></div>
<p class="title">${this.hasError ? this.status : this.loadingText}</p>
${this.hasError
? html`<pmui-button id="cancel-btn" pmui-width="200px" pmui-label="Cancel" @pmui-click=${() => window.location.reload()}></pmui-button>`
: ''
}
`
}
}
window.customElements.define('pmui-loadingscreen', LoadingScreen)

144
lib/menus/options_screen.js Normal file
View file

@ -0,0 +1,144 @@
const { LitElement, html, css } = require('lit')
const { commonCss, displayScreen } = require('./components/common')
class OptionsScreen extends LitElement {
static get styles () {
return css`
${commonCss}
.title {
top: 4px;
}
main {
display: flex;
flex-direction: column;
position: absolute;
top: calc(100% / 6 - 6px);
left: 50%;
width: 310px;
gap: 4px 0;
place-items: center;
place-content: center;
transform: translate(-50%);
}
.wrapper {
display: flex;
flex-direction: row;
width: 100%;
gap: 0 10px;
height: 20px;
}
`
}
static get properties () {
return {
isInsideWorld: { type: Boolean },
mouseSensitivityX: { type: Number },
mouseSensitivityY: { type: Number },
chatWidth: { type: Number },
chatHeight: { type: Number },
chatScale: { type: Number },
sound: { type: Number },
renderDistance: { type: Number },
fov: { type: Number },
guiScale: { type: Number }
}
}
constructor () {
super()
this.isInsideWorld = false
const getValue = (item, defaultValue, convertFn) => window.localStorage.getItem(item) ? convertFn(window.localStorage.getItem(item)) : defaultValue
this.mouseSensitivityX = getValue('mouseSensX', 50, (v) => Math.floor(Number(v) * 10000))
this.mouseSensitivityY = getValue('mouseSensY', 50, (v) => Math.floor(Number(v) * 10000))
this.chatWidth = getValue('chatWidth', 320, (v) => Number(v))
this.chatHeight = getValue('chatHeight', 180, (v) => Number(v))
this.chatScale = getValue('chatScale', 100, (v) => Number(v))
this.sound = getValue('sound', 50, (v) => Number(v))
this.renderDistance = getValue('renderDistance', 6, (v) => Number(v))
this.fov = getValue('fov', 75, (v) => Number(v))
this.guiScale = getValue('guiScale', 3, (v) => Number(v))
document.documentElement.style.setProperty('--chatScale', `${this.chatScale}`)
document.documentElement.style.setProperty('--chatWidth', `${this.chatWidth}px`)
document.documentElement.style.setProperty('--chatHeight', `${this.chatHeight}px`)
document.documentElement.style.setProperty('--guiScale', `${this.guiScale}`)
}
render () {
return html`
<div class="${this.isInsideWorld ? 'bg' : 'dirt-bg'}"></div>
<p class="title">Options</p>
<main>
<div class="wrapper">
<pmui-slider pmui-label="Mouse Sensitivity X" pmui-value="${this.mouseSensitivityX}" pmui-min="1" pmui-max="100" @input=${(e) => {
this.mouseSensitivityX = Number(e.target.value)
window.localStorage.setItem('mouseSensX', this.mouseSensitivityX * 0.0001)
}}></pmui-slider>
<pmui-slider pmui-label="Mouse Sensitivity Y" pmui-value="${this.mouseSensitivityY}" pmui-min="1" pmui-max="100" @input=${(e) => {
this.mouseSensitivityY = Number(e.target.value)
window.localStorage.setItem('mouseSensY', this.mouseSensitivityY * 0.0001)
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-slider pmui-label="Chat Width" pmui-value="${this.chatWidth}" pmui-min="0" pmui-max="320" pmui-type="px" @input=${(e) => {
this.chatWidth = Number(e.target.value)
window.localStorage.setItem('chatWidth', `${this.chatWidth}`)
document.documentElement.style.setProperty('--chatWidth', `${this.chatWidth}px`)
}}></pmui-slider>
<pmui-slider pmui-label="Chat Height" pmui-value="${this.chatHeight}" pmui-min="0" pmui-max="180" pmui-type="px" @input=${(e) => {
this.chatHeight = Number(e.target.value)
window.localStorage.setItem('chatHeight', `${this.chatHeight}`)
document.documentElement.style.setProperty('--chatHeight', `${this.chatHeight}px`)
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-slider pmui-label="Chat Scale" pmui-value="${this.chatScale}" pmui-min="0" pmui-max="100" @input=${(e) => {
this.chatScale = Number(e.target.value)
window.localStorage.setItem('chatScale', `${this.chatScale}`)
document.documentElement.style.setProperty('--chatScale', `${this.chatScale}`)
}}></pmui-slider>
<pmui-slider pmui-label="Sound Volume" pmui-value="${this.sound}" pmui-min="0" pmui-max="100" @input=${(e) => {
this.sound = Number(e.target.value)
window.localStorage.setItem('sound', `${this.sound}`)
}}></pmui-slider>
</div>
<div class="wrapper">
<pmui-button pmui-width="150px" pmui-label="Key Binds" @pmui-click=${() => displayScreen(this, document.getElementById('keybinds-screen'))}></pmui-button>
<pmui-slider pmui-label="Gui Scale" pmui-value="${this.guiScale}" pmui-min="1" pmui-max="4" pmui-type="" @input=${(e) => {
this.guiScale = Number(e.target.value)
window.localStorage.setItem('guiScale', `${this.guiScale}`)
document.documentElement.style.setProperty('--guiScale', `${this.guiScale}`)
}}></pmui-slider>
</div>
${this.isInsideWorld
? ''
: html`
<div class="wrapper">
<pmui-slider pmui-label="Render Distance" pmui-value="${this.renderDistance}" pmui-min="2" pmui-max="6" pmui-type=" chunks" @input=${(e) => {
this.renderDistance = Number(e.target.value)
window.localStorage.setItem('renderDistance', `${this.renderDistance}`)
}}></pmui-slider>
<pmui-slider pmui-label="Field of View" pmui-value="${this.fov}" pmui-min="30" pmui-max="110" pmui-type="" @input=${(e) => {
this.fov = Number(e.target.value)
window.localStorage.setItem('fov', `${this.fov}`)
this.dispatchEvent(new window.CustomEvent('fov_changed', { fov: this.fov }))
}}></pmui-slider>
</div>
`}
<br>
<pmui-button pmui-width="200px" pmui-label="Done" @pmui-click=${() => displayScreen(this, document.getElementById(this.isInsideWorld ? 'pause-screen' : 'title-screen'))}></pmui-button>
</main>
`
}
}
window.customElements.define('pmui-optionsscreen', OptionsScreen)

110
lib/menus/pause_screen.js Normal file
View file

@ -0,0 +1,110 @@
const { LitElement, html, css } = require('lit')
const { openURL, displayScreen } = require('./components/common')
class PauseScreen extends LitElement {
static get styles () {
return css`
.bg {
position: absolute;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.75);
width: 100%;
height: 100%;
}
.title {
position: absolute;
top: 40px;
left: 50%;
transform: translate(-50%);
font-size: 10px;
color: white;
text-shadow: 1px 1px #222;
}
main {
display: flex;
flex-direction: column;
gap: 4px 0;
position: absolute;
left: 50%;
width: 204px;
top: calc(25% + 48px - 16px);
transform: translate(-50%);
}
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
`
}
constructor () {
super()
this.inMenu = false
}
init (renderer) {
const chat = document.getElementById('hud').shadowRoot.querySelector('#chat')
const self = this
document.addEventListener('keydown', e => {
if (chat.inChat) return
e = e || window.event
if (e.keyCode === 27 || e.key === 'Escape' || e.key === 'Esc') {
if (self.inMenu) {
self.disableGameMenu(renderer)
} else {
self.enableGameMenu()
}
}
})
}
render () {
return html`
<div class="bg"></div>
<p class="title">Game Menu</p>
<main>
<pmui-button pmui-width="204px" pmui-label="Back to Game" @pmui-click=${this.onReturnPress}></pmui-button>
<div class="row">
<pmui-button pmui-width="98px" pmui-label="Github" @pmui-click=${() => openURL('https://github.com/PrismarineJS/prismarine-web-client')}></pmui-button>
<pmui-button pmui-width="98px" pmui-label="Discord" @pmui-click=${() => openURL('https://discord.gg/4Ucm684Fq3')}></pmui-button>
</div>
<pmui-button pmui-width="204px" pmui-label="Options" @pmui-click=${() => displayScreen(this, document.getElementById('options-screen'))}></pmui-button>
<pmui-button pmui-width="204px" pmui-label="Disconnect" @pmui-click=${() => window.location.reload()}></pmui-button>
</main>
`
}
disableGameMenu (renderer = false) {
this.inMenu = false
this.style.display = 'none'
if (renderer) {
renderer.domElement.requestPointerLock()
}
}
enableGameMenu () {
this.inMenu = true
document.exitPointerLock()
this.style.display = 'block'
this.focus()
}
onReturnPress (renderer = false) {
this.inMenu = false
this.style.display = 'none'
if (renderer) {
renderer.domElement.requestPointerLock()
}
}
}
window.customElements.define('pmui-pausescreen', PauseScreen)

142
lib/menus/play_screen.js Normal file
View file

@ -0,0 +1,142 @@
const { LitElement, html, css } = require('lit')
const { commonCss, displayScreen } = require('./components/common')
class PlayScreen extends LitElement {
static get styles () {
return css`
${commonCss}
.title {
top: 12px;
}
.edit-boxes {
position: absolute;
top: 59px;
left: 50%;
display: flex;
flex-direction: column;
gap: 14px 0;
transform: translate(-50%);
width: 310px;
}
.wrapper {
width: 100%;
display: flex;
flex-direction: row;
gap: 0 4px;
}
.button-wrapper {
display: flex;
flex-direction: row;
gap: 0 4px;
position: absolute;
bottom: 9px;
left: 50%;
transform: translate(-50%);
width: 310px;
}
`
}
static get properties () {
return {
server: { type: String },
serverport: { type: Number },
proxy: { type: String },
proxyport: { type: Number },
username: { type: String },
password: { type: String }
}
}
constructor () {
super()
this.server = ''
this.serverport = 25565
this.proxy = ''
this.proxyport = ''
this.username = window.localStorage.getItem('username') ?? 'pviewer' + (Math.floor(Math.random() * 1000))
this.password = ''
window.fetch('config.json').then(res => res.json()).then(config => {
this.server = config.defaultHost
this.serverport = config.defaultHostPort ?? 25565
this.proxy = config.defaultProxy
this.proxyport = !config.defaultProxy && !config.defaultProxyPort ? '' : config.defaultProxyPort ?? 443
})
}
render () {
return html`
<div class="dirt-bg"></div>
<p class="title">Join a Server</p>
<main class="edit-boxes">
<div class="wrapper">
<pmui-editbox
pmui-width="150px"
pmui-label="Server IP"
pmui-id="serverip"
pmui-value="${this.server}"
@input=${e => { this.server = e.target.value }}
></pmui-editbox>
<pmui-editbox
pmui-width="150px"
pmui-label="Server Port"
pmui-id="port"
pmui-value="${this.serverport}"
@input=${e => { this.serverport = e.target.value }}
></pmui-editbox>
</div>
<div class="wrapper">
<pmui-editbox
pmui-width="150px"
pmui-label="Proxy"
pmui-id="proxy"
pmui-value="${this.proxy}"
@input=${e => { this.proxy = e.target.value }}
></pmui-editbox>
<pmui-editbox
pmui-width="150px"
pmui-label="Port"
pmui-id="port"
pmui-value="${this.proxyport}"
@input=${e => { this.proxyport = e.target.value }}
></pmui-editbox>
</div>
<div class="wrapper">
<pmui-editbox
pmui-width="150px"
pmui-label="Username"
pmui-id="username"
pmui-value="${this.username}"
@input=${e => { this.username = e.target.value }}
></pmui-editbox>
</div>
</main>
<div class="button-wrapper">
<pmui-button pmui-width="150px" pmui-label="Connect" @pmui-click=${this.onConnectPress}></pmui-button>
<pmui-button pmui-width="150px" pmui-label="Cancel" @pmui-click=${() => displayScreen(this, document.getElementById('title-screen'))}></pmui-button>
</div>
`
}
onConnectPress () {
window.localStorage.setItem('username', this.username)
this.dispatchEvent(new window.CustomEvent('connect', {
detail: {
server: `${this.server}:${this.serverport}`,
proxy: `${this.proxy}${this.proxy !== '' ? `:${this.proxyport}` : ''}`,
username: this.username,
password: this.password
}
}))
}
}
window.customElements.define('pmui-playscreen', PlayScreen)

124
lib/menus/title_screen.js Normal file
View file

@ -0,0 +1,124 @@
const { openURL, displayScreen } = require('./components/common')
const { LitElement, html, css } = require('lit')
class TitleScreen extends LitElement {
static get styles () {
return css`
.minecraft {
position: absolute;
top: 30px;
left: calc(50% - 137px);
}
.minecraft .minec {
display: block;
position: absolute;
top: 0;
left: 0;
background-image: url('textures/1.17.1/gui/title/minecraft.png');
background-size: 256px;
width: 155px;
height: 44px;
}
.minecraft .raft {
display: block;
position: absolute;
top: 0;
left: 155px;
background-image: url('textures/1.17.1/gui/title/minecraft.png');
background-size: 256px;
width: 155px;
height: 44px;
background-position-y: -45px;
}
.minecraft .edition {
display: block;
position: absolute;
top: 37px;
left: calc(88px + 5px);
background-image: url('extra-textures/edition.png');
background-size: 128px;
width: 88px;
height: 14px;
}
.splash {
position: absolute;
top: 32px;
left: 227px;
color: #ff0;
transform: translate(-50%, -50%) rotateZ(-20deg) scale(1);
width: max-content;
text-shadow: 1px 1px #220;
font-size: 10px;
animation: splashAnim 400ms infinite alternate linear;
}
@keyframes splashAnim {
to {
transform: translate(-50%, -50%) rotateZ(-20deg) scale(1.07);
}
}
.menu {
display: flex;
flex-direction: column;
gap: 4px 0;
position: absolute;
top: calc(25% + 48px);
left: 50%;
width: 200px;
transform: translate(-50%);
}
.menu-row {
display: flex;
flex-direction: row;
gap: 0 4px;
width: 100%;
}
.bottom-info {
display: flex;
flex-direction: row;
justify-content: space-between;
position: absolute;
bottom: -1px;
left: 1px;
width: calc(100% - 2px);
color: white;
text-shadow: 1px 1px #222;
font-size: 10px;
}
`
}
render () {
return html`
<div class="minecraft">
<div class="minec"></div>
<div class="raft"></div>
<div class="edition"></div>
<span class="splash">Prismarine is a beautiful block</span>
</div>
<div class="menu">
<pmui-button pmui-width="200px" pmui-label="Play" @pmui-click=${() => displayScreen(this, document.getElementById('play-screen'))}></pmui-button>
<pmui-button pmui-width="200px" pmui-label="Options" @pmui-click=${() => displayScreen(this, document.getElementById('options-screen'))}></pmui-button>
<div class="menu-row">
<pmui-button pmui-width="98px" pmui-label="Github" @pmui-click=${() => openURL('https://github.com/PrismarineJS/prismarine-web-client')}></pmui-button>
<pmui-button pmui-width="98px" pmui-label="Discord" @pmui-click=${() => openURL('https://discord.gg/4Ucm684Fq3')}></pmui-button>
</div>
</div>
<div class="bottom-info">
<span>Prismarine Web Client</span>
<span>A minecraft client in the browser!</span>
</div>
`
}
}
window.customElements.define('pmui-titlescreen', TitleScreen)

View file

@ -1,118 +0,0 @@
const { LitElement, html, css } = require('lit')
class PlayerList extends LitElement {
static get styles () {
return css`
.playerlist-container {
position: absolute;
background-color: rgba(0, 0, 0, 0.5);
left: 50%;
top: 1%;
transform: translateX(-50%);
border: 2px solid rgba(0, 0, 0, 0.8);
padding: 5px;
min-width: 8%;
}
.playerlist-entry {
overflow: hidden;
color: white;
font-size: 20px;
margin: 0px;
line-height: 100%;
text-shadow: 2px 2px 0px #3f3f3f;
font-family: mojangles, minecraft, monospace;
width: 100%;
}
.plist-active-player {
color: rgb(42, 204, 237);
}
.plist-ping-container {
text-align: right;
float: right;
padding-left: 20px;
}
.plist-ping-value {
color: rgb(114, 255, 114);
float: left;
margin: 0px;
}
.plist-ping-label {
color: white;
float: right;
margin: 0px;
}
`
}
static get properties () {
return {
clientId: { type: String },
players: { type: Object }
}
}
constructor () {
super()
this.clientId = ''
this.players = {}
}
render () {
return html`
<div id="playerlist-container" class="playerlist-container" style="display: none;">
${Object.values(this.players).map(player => html`
<div class="playerlist-entry${this.clientId === player.uuid ? ' plist-active-player' : ''}" id="plist-player-${player.uuid}">
${player.username}
<div class="plist-ping-container">
<p class="plist-ping-value">${player.ping}</p>
<p class="plist-ping-label">ms</p>
</div>
</div>
`)}
</div>
`
}
init (bot) {
const playerList = this.shadowRoot.querySelector('#playerlist-container')
this.isOpen = false
this.players = bot.players
this.clientId = bot.player.uuid
this.requestUpdate()
const showList = (shouldShow = true) => {
playerList.style.display = shouldShow ? 'block' : 'none'
this.isOpen = shouldShow
}
document.addEventListener('keydown', e => {
e ??= window.event
if (e.key === 'Tab') {
showList(true)
e.preventDefault()
}
})
document.addEventListener('keyup', e => {
if (!this.isOpen) return
e ??= window.event
if (e.key === 'Tab') {
showList(false)
e.preventDefault()
}
})
bot.on('playerUpdated', () => this.requestUpdate()) // LitElement seems to be batching requests, so it should be fine?
bot.on('playerJoined', () => this.requestUpdate())
bot.on('playerLeft', () => this.requestUpdate())
}
}
window.customElements.define('player-list', PlayerList)

View file

@ -6,7 +6,7 @@
"scripts": {
"build": "webpack --config webpack.prod.js",
"build-dev": "webpack --config webpack.dev.js",
"start": "node server.js 8080 dev",
"start": "node --max-old-space-size=8192 server.js 8080 dev",
"prod-start": "node server.js",
"build-dev-start": "npm run build-dev && npm run prod-start",
"build-start": "npm run build && npm run prod-start",

View file

@ -1,12 +1,36 @@
:root {
--guiScaleFactor: 3;
--guiScale: 3;
--chatWidth: 320px;
--chatHeight: 180px;
--chatScale: 1;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
height: 100%;
height: 100vh;
overflow: hidden;
}
.dirt-bg {
position: absolute;
top: 0;
left: 0;
background: url('textures/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.7);
background-size: 16px;
background-repeat: repeat;
width: 100%;
height: 100%;
transform-origin: top left;
transform: scale(2);
background-blend-mode: overlay;
}
@font-face {
font-family: minecraft;
src: url(minecraftia.woff);
@ -16,13 +40,16 @@ html {
font-family: mojangles;
src: url(mojangles.ttf);
}
body {
overflow: hidden;
position: relative;
margin:0;
padding:0;
height: 100vh;
font-family: sans-serif;
background: linear-gradient(#141e30, #243b55);
background: #333;
/* background: linear-gradient(#141e30, #243b55); */
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
@ -32,9 +59,30 @@ body {
}
canvas {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
font-size: 0;
margin: 0;
padding: 0;
}
#ui-root {
position: absolute;
top: 0;
left: 0;
transform-origin: top left;
transform: scale(var(--guiScale));
width: calc(100% / var(--guiScale));
height: calc(100% / var(--guiScale));
z-index: 10;
image-rendering: optimizeSpeed;
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-optimize-contrast;
image-rendering: -o-crisp-edges;
image-rendering: pixelated;
-ms-interpolation-mode: nearest-neighbor;
font-family: minecraft, mojangles, monospace;
}