enable strict null types which improves quality of codebase a lot!
todo add types to worker & world renderer
This commit is contained in:
parent
7c5af2da0d
commit
d546fa8f41
31 changed files with 150 additions and 93 deletions
|
|
@ -21,7 +21,7 @@ export class Viewer {
|
|||
isSneaking: boolean
|
||||
version: string
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, numWorkers = undefined) {
|
||||
constructor (public renderer: THREE.WebGLRenderer, numWorkers?: number) {
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.background = new THREE.Color('lightblue')
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ export class Viewer {
|
|||
this.primitives.update(p)
|
||||
}
|
||||
|
||||
setFirstPersonCamera (pos: Vec3, yaw: number, pitch: number, roll = 0) {
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
|
||||
if (pos) {
|
||||
let y = pos.y + this.playerHeight
|
||||
if (this.isSneaking) y -= 0.3
|
||||
|
|
|
|||
|
|
@ -158,6 +158,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => {
|
||||
const pos = new Vec3((botX + x) * 16, 0, (botZ + z) * 16)
|
||||
if (!this.loadedChunks[`${pos.x},${pos.z}`]) return pos
|
||||
return undefined!
|
||||
}).filter(Boolean)
|
||||
this.lastPos.update(pos)
|
||||
await this._loadChunks(positions)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { versionToNumber } from './utils'
|
|||
const legacyInvsprite = JSON.parse(fs.readFileSync(join(__dirname, '../../../src/invsprite.json'), 'utf8'))
|
||||
|
||||
//@ts-ignore
|
||||
const latestMcAssetsVersion = McAssets.versions.at(-1)
|
||||
const latestMcAssetsVersion = McAssets.versions.at(-1)!
|
||||
// const latestVersion = minecraftDataLoader.supportedVersions.pc.at(-1)
|
||||
const mcData = minecraftDataLoader(latestMcAssetsVersion)
|
||||
const PBlock = BlockLoader(latestMcAssetsVersion)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
|
|||
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
||||
for (let [lineNum, text] of texts.slice(0, 4).entries()) {
|
||||
// todo: in pre flatenning it seems the format was not json
|
||||
const parsed = text.startsWith('{') ? parseSafe(text ?? '""', 'sign text') : text
|
||||
const parsed = text?.startsWith('{') ? parseSafe(text ?? '""', 'sign text') : text
|
||||
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) continue
|
||||
// todo fix type
|
||||
const message = typeof parsed === 'string' ? fromFormattedString(parsed) : new PrismarineChat(parsed) as never
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ let audioContext: AudioContext
|
|||
const sounds: Record<string, any> = {}
|
||||
|
||||
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
||||
const loadingSounds = []
|
||||
const convertedSounds = []
|
||||
const loadingSounds = [] as string[]
|
||||
const convertedSounds = [] as string[]
|
||||
export async function loadSound (path: string) {
|
||||
if (loadingSounds.includes(path)) return
|
||||
loadingSounds.push(path)
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mk
|
|||
})
|
||||
//@ts-expect-error
|
||||
fs.promises.open = async (...args) => {
|
||||
//@ts-expect-error
|
||||
const fd = await promisify(fs.open)(...args)
|
||||
return {
|
||||
...Object.fromEntries(['read', 'write', 'close'].map(x => [x, async (...args) => {
|
||||
|
|
@ -127,7 +128,7 @@ export const mkdirRecursive = async (path: string) => {
|
|||
|
||||
export const uniqueFileNameFromWorldName = async (title: string, savePath: string) => {
|
||||
const name = sanitizeFilename(title)
|
||||
let resultPath: string
|
||||
let resultPath!: string
|
||||
// getUniqueFolderName
|
||||
let i = 0
|
||||
let free = false
|
||||
|
|
@ -176,7 +177,7 @@ export const mountExportFolder = async () => {
|
|||
}
|
||||
|
||||
export async function removeFileRecursiveAsync (path) {
|
||||
const errors = []
|
||||
const errors = [] as Array<[string, Error]>
|
||||
try {
|
||||
const files = await fs.promises.readdir(path)
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const exportWorld = async (path: string, type: 'zip' | 'folder', zipName
|
|||
setLoadingScreenStatus('Preparing export folder')
|
||||
let dest = '/'
|
||||
if ((await fs.promises.readdir('/export')).length) {
|
||||
const { levelDat } = await readLevelDat(path)
|
||||
const { levelDat } = (await readLevelDat(path))!
|
||||
dest = await uniqueFileNameFromWorldName(levelDat.LevelName, path)
|
||||
}
|
||||
setLoadingScreenStatus(`Copying files to ${dest} of selected folder`)
|
||||
|
|
|
|||
|
|
@ -229,6 +229,7 @@ class ChatBox extends LitElement {
|
|||
}
|
||||
|
||||
notification.show = false
|
||||
// @ts-expect-error
|
||||
const chat = this.shadowRoot.getElementById('chat-messages')
|
||||
/** @type {HTMLInputElement} */
|
||||
// @ts-expect-error
|
||||
|
|
@ -258,10 +259,12 @@ class ChatBox extends LitElement {
|
|||
* @param {import('minecraft-protocol').Client} client
|
||||
*/
|
||||
init (client) {
|
||||
// @ts-expect-error
|
||||
const chat = this.shadowRoot.getElementById('chat-messages')
|
||||
/** @type {HTMLInputElement} */
|
||||
// @ts-expect-error
|
||||
const chatInput = this.shadowRoot.getElementById('chatinput')
|
||||
/** @type {any} */
|
||||
this.chatInput = chatInput
|
||||
|
||||
// Show chat
|
||||
|
|
@ -330,6 +333,7 @@ class ChatBox extends LitElement {
|
|||
fading: false,
|
||||
faded: false
|
||||
}]
|
||||
/** @type {any} */
|
||||
const message = this.messages.at(-1)
|
||||
|
||||
chat.scrollTop = chat.scrollHeight // Stay bottom of the list
|
||||
|
|
@ -358,6 +362,7 @@ class ChatBox extends LitElement {
|
|||
const completeValue = this.getCompleteValue()
|
||||
this.completePadText = completeValue === '/' ? '' : completeValue
|
||||
if (this.completeRequestValue === completeValue) {
|
||||
/** @type {any} */
|
||||
const lastWord = chatInput.value.split(' ').at(-1)
|
||||
this.completionItems = this.completionItemsSource.filter(i => {
|
||||
const compareableParts = i.split(/[_:]/)
|
||||
|
|
@ -425,6 +430,7 @@ class ChatBox extends LitElement {
|
|||
}
|
||||
|
||||
/** @type {string[]} */
|
||||
// @ts-expect-error
|
||||
const applyStyles = [
|
||||
color ? colorF(color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${getColorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : messageFormatStylesMap.white,
|
||||
italic && messageFormatStylesMap.italic,
|
||||
|
|
@ -458,6 +464,7 @@ class ChatBox extends LitElement {
|
|||
}
|
||||
|
||||
updateInputValue (value) {
|
||||
/** @type {any} */
|
||||
const { chatInput } = this
|
||||
chatInput.value = value
|
||||
chatInput.dispatchEvent(new Event('input'))
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ export const contro = new ControMax({
|
|||
prevHotbarSlot: [null, 'Right Bumper'],
|
||||
attackDestroy: [null, 'Right Trigger'],
|
||||
interactPlace: [null, 'Left Trigger'],
|
||||
chat: [['KeyT', 'Enter'], null],
|
||||
command: ['Slash', null],
|
||||
chat: [['KeyT', 'Enter']],
|
||||
command: ['Slash'],
|
||||
},
|
||||
ui: {
|
||||
back: [null/* 'Escape' */, 'B'],
|
||||
|
|
@ -81,7 +81,8 @@ contro.on('movementUpdate', ({ vector, gamepadIndex }) => {
|
|||
if (v === undefined || Math.abs(v) < 0.3) continue
|
||||
// todo use raw values eg for slow movement
|
||||
const mappedValue = v < 0 ? -1 : 1
|
||||
const foundAction = coordToAction.find(([c, mapV]) => c === coord && mapV === mappedValue)?.[2]
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
||||
const foundAction = coordToAction.find(([c, mapV]) => c === coord && mapV === mappedValue)?.[2]!
|
||||
newState[foundAction] = true
|
||||
}
|
||||
|
||||
|
|
@ -223,13 +224,14 @@ contro.on('release', ({ command }) => {
|
|||
|
||||
const hardcodedPressedKeys = new Set<string>()
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!isGameActive(false)) return
|
||||
if (hardcodedPressedKeys.has('F3')) {
|
||||
// reload chunks
|
||||
if (e.code === 'KeyA') {
|
||||
//@ts-expect-error
|
||||
const loadedChunks = Object.entries(worldView.loadedChunks).filter(([, v]) => v).map(([key]) => key.split(',').map(Number))
|
||||
for (const [x, z] of loadedChunks) {
|
||||
worldView.unloadChunk({ x, z })
|
||||
worldView!.unloadChunk({ x, z })
|
||||
}
|
||||
if (localServer) {
|
||||
localServer.players[0].world.columns = {}
|
||||
|
|
@ -280,7 +282,11 @@ const startFlyLoop = () => {
|
|||
endFlyLoop?.()
|
||||
|
||||
endFlyLoop = makeInterval(() => {
|
||||
if (!bot) endFlyLoop()
|
||||
if (!bot) {
|
||||
endFlyLoop?.()
|
||||
return
|
||||
}
|
||||
|
||||
bot.entity.position.add(currentFlyVector.clone().multiply(new Vec3(0, 0.5, 0)))
|
||||
}, 50)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { options } from './optionsStorage'
|
||||
import { assertDefined } from './utils'
|
||||
|
||||
export default () => {
|
||||
bot.on('time', () => {
|
||||
assertDefined(viewer)
|
||||
// 0 morning
|
||||
const dayTotal = 24_000
|
||||
const evening = 12_542
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ const getFixedFilesize = (bytes: number) => {
|
|||
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
const inner = async () => {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
let mapUrl = qs.get('map')
|
||||
const texturepack = qs.get('texturepack')
|
||||
|
|
@ -33,13 +33,15 @@ export default async () => {
|
|||
if (!contentType || !contentType.startsWith('application/zip')) {
|
||||
alert('Invalid map file')
|
||||
}
|
||||
const contentLength = +response.headers.get('Content-Length')
|
||||
setLoadingScreenStatus(`Downloading ${downloadThing} ${name}: have to download ${getFixedFilesize(contentLength)}...`)
|
||||
const contentLengthStr = response.headers?.get('Content-Length')
|
||||
const contentLength = contentLengthStr && +contentLengthStr
|
||||
setLoadingScreenStatus(`Downloading ${downloadThing} ${name}: have to download ${contentLength && getFixedFilesize(contentLength)}...`)
|
||||
|
||||
let downloadedBytes = 0
|
||||
const buffer = await new Response(
|
||||
new ReadableStream({
|
||||
async start (controller) {
|
||||
if (!response.body) throw new Error('Server returned no response!')
|
||||
const reader = response.body.getReader()
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
|
|
@ -54,12 +56,9 @@ export default async () => {
|
|||
downloadedBytes += value.byteLength
|
||||
|
||||
// Calculate download progress as a percentage
|
||||
const progress = (downloadedBytes / contentLength) * 100
|
||||
const progress = contentLength ? (downloadedBytes / contentLength) * 100 : undefined
|
||||
setLoadingScreenStatus(`Download ${downloadThing} progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${contentLength && getFixedFilesize(contentLength)})`, false, true)
|
||||
|
||||
// Update your progress bar or display the progress value as needed
|
||||
if (contentLength) {
|
||||
setLoadingScreenStatus(`Download ${downloadThing} progress: ${Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${getFixedFilesize(contentLength)})`, false, true)
|
||||
}
|
||||
|
||||
// Pass the received data to the controller
|
||||
controller.enqueue(value)
|
||||
|
|
@ -74,3 +73,12 @@ export default async () => {
|
|||
await openWorldZip(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
try {
|
||||
return await inner()
|
||||
} catch (err) {
|
||||
setLoadingScreenStatus(`Failed to download. Either refresh page or remove mapUrl param from URL. Reason: ${err.message}`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const hideModal = (modal = activeModalStack.at(-1), data: any = undefined
|
|||
}
|
||||
}
|
||||
|
||||
export const hideCurrentModal = (_data = undefined, onHide = undefined) => {
|
||||
export const hideCurrentModal = (_data?, onHide?: () => void) => {
|
||||
if (hideModal(undefined, undefined)) {
|
||||
onHide?.()
|
||||
}
|
||||
|
|
|
|||
27
src/index.ts
27
src/index.ts
|
|
@ -110,7 +110,7 @@ viewer.entities.entitiesOptions = {
|
|||
watchOptionsAfterViewerInit()
|
||||
watchTexturepackInViewer(viewer)
|
||||
|
||||
let renderInterval: number
|
||||
let renderInterval: number | false
|
||||
watchValue(options, (o) => {
|
||||
renderInterval = o.frameLimit && 1000 / o.frameLimit
|
||||
})
|
||||
|
|
@ -214,7 +214,7 @@ function listenGlobalEvents () {
|
|||
})
|
||||
}
|
||||
|
||||
let listeners = []
|
||||
let listeners = [] as Array<{ target, event, callback }>
|
||||
// only for dom listeners (no removeAllListeners)
|
||||
// todo refactor them out of connect fn instead
|
||||
const registerListener: import('./utilsTs').RegisterListener = (target, event, callback) => {
|
||||
|
|
@ -241,7 +241,7 @@ const cleanConnectIp = (host: string | undefined, defaultPort: string | undefine
|
|||
}
|
||||
|
||||
async function connect (connectOptions: {
|
||||
server?: string; singleplayer?: any; username?: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; peerId?: string
|
||||
server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; peerId?: string
|
||||
}) {
|
||||
document.getElementById('play-screen').style = 'display: none;'
|
||||
removePanorama()
|
||||
|
|
@ -261,7 +261,7 @@ async function connect (connectOptions: {
|
|||
setLoadingScreenStatus('Logging in')
|
||||
|
||||
let ended = false
|
||||
let bot: typeof __type_bot
|
||||
let bot!: typeof __type_bot
|
||||
const destroyAll = () => {
|
||||
if (ended) return
|
||||
ended = true
|
||||
|
|
@ -275,7 +275,9 @@ async function connect (connectOptions: {
|
|||
bot.emit('end', '')
|
||||
bot.removeAllListeners()
|
||||
bot._client.removeAllListeners()
|
||||
//@ts-expect-error TODO?
|
||||
bot._client = undefined
|
||||
//@ts-expect-error
|
||||
window.bot = bot = undefined
|
||||
}
|
||||
if (singleplayer && !fsState.inMemorySave) {
|
||||
|
|
@ -394,10 +396,10 @@ async function connect (connectOptions: {
|
|||
setLoadingScreenStatus(initialLoadingText)
|
||||
bot = mineflayer.createBot({
|
||||
host: server.host,
|
||||
port: +server.port,
|
||||
port: server.port ? +server.port : undefined,
|
||||
version: connectOptions.botVersion || false,
|
||||
...p2pMultiplayer ? {
|
||||
stream: await connectToPeer(connectOptions.peerId),
|
||||
stream: await connectToPeer(connectOptions.peerId!),
|
||||
} : {},
|
||||
...singleplayer || p2pMultiplayer ? {
|
||||
keepAlive: false,
|
||||
|
|
@ -495,6 +497,7 @@ async function connect (connectOptions: {
|
|||
onBotCreate()
|
||||
|
||||
bot.once('login', () => {
|
||||
if (!connectOptions.server) return
|
||||
// server is ok, add it to the history
|
||||
const serverHistory: string[] = JSON.parse(localStorage.getItem('serverHistory') || '[]')
|
||||
serverHistory.unshift(connectOptions.server)
|
||||
|
|
@ -517,7 +520,7 @@ async function connect (connectOptions: {
|
|||
|
||||
const center = bot.entity.position
|
||||
|
||||
const worldView = window.worldView = new WorldDataEmitter(bot.world, singleplayer ? renderDistance : Math.min(renderDistance, maxMultiplayerRenderDistance), center)
|
||||
const worldView = window.worldView = new WorldDataEmitter(bot.world, singleplayer ? renderDistance : Math.min(renderDistance, maxMultiplayerRenderDistance!), center)
|
||||
setRenderDistance()
|
||||
|
||||
bot.on('physicsTick', () => updateCursor())
|
||||
|
|
@ -537,9 +540,9 @@ async function connect (connectOptions: {
|
|||
|
||||
try {
|
||||
const gl = renderer.getContext()
|
||||
debugMenu.rendererDevice = gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL)
|
||||
debugMenu.rendererDevice = gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
console.warn(err)
|
||||
debugMenu.rendererDevice = '???'
|
||||
}
|
||||
|
||||
|
|
@ -554,7 +557,7 @@ async function connect (connectOptions: {
|
|||
function botPosition () {
|
||||
// this might cause lag, but not sure
|
||||
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
worldView.updatePosition(bot.entity.position)
|
||||
void worldView.updatePosition(bot.entity.position)
|
||||
}
|
||||
bot.on('move', botPosition)
|
||||
botPosition()
|
||||
|
|
@ -585,7 +588,7 @@ async function connect (connectOptions: {
|
|||
let virtualClickActive = false
|
||||
let virtualClickTimeout
|
||||
let screenTouches = 0
|
||||
let capturedPointer: { id; x; y; sourceX; sourceY; activateCameraMove; time } | null
|
||||
let capturedPointer: { id; x; y; sourceX; sourceY; activateCameraMove; time } | undefined
|
||||
registerListener(document, 'pointerdown', (e) => {
|
||||
const clickedEl = e.composedPath()[0]
|
||||
if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || e.pointerId === undefined) {
|
||||
|
|
@ -731,7 +734,7 @@ downloadAndOpenFile().then((downloadAction) => {
|
|||
const peerId = qs.get('connectPeer')
|
||||
const version = qs.get('peerVersion')
|
||||
if (peerId) {
|
||||
let username = options.guestUsername
|
||||
let username: string | null = options.guestUsername
|
||||
if (options.askGuestName) username = prompt('Enter your username', username)
|
||||
if (!username) return
|
||||
options.guestUsername = username
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const readLevelDat = async (path) => {
|
|||
}
|
||||
const { parsed } = await nbt.parse(Buffer.from(levelDatContent))
|
||||
const levelDat: import('./mcTypes').LevelDat = nbt.simplify(parsed).Data
|
||||
return { levelDat, dataRaw: parsed.value.Data.value as Record<string, any> }
|
||||
return { levelDat, dataRaw: parsed.value.Data!.value as Record<string, any> }
|
||||
}
|
||||
|
||||
export const loadSave = async (root = '/world') => {
|
||||
|
|
@ -54,7 +54,7 @@ export const loadSave = async (root = '/world') => {
|
|||
// todo check jsHeapSizeLimit
|
||||
|
||||
const warnings: string[] = []
|
||||
const { levelDat, dataRaw } = await readLevelDat(root)
|
||||
const { levelDat, dataRaw } = (await readLevelDat(root))!
|
||||
if (levelDat === undefined) {
|
||||
if (fsState.isReadonly) {
|
||||
throw new Error('level.dat not found, ensure you are loading world folder')
|
||||
|
|
@ -63,7 +63,7 @@ export const loadSave = async (root = '/world') => {
|
|||
}
|
||||
}
|
||||
|
||||
let version: string | undefined
|
||||
let version: string | undefined | null
|
||||
let isFlat = false
|
||||
if (levelDat) {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
|
|
@ -73,7 +73,7 @@ export const loadSave = async (root = '/world') => {
|
|||
if (!newVersion) return
|
||||
version = newVersion
|
||||
}
|
||||
const lastSupportedVersion = supportedVersions.at(-1)
|
||||
const lastSupportedVersion = supportedVersions.at(-1)!
|
||||
const firstSupportedVersion = supportedVersions[0]
|
||||
const lowerBound = isMajorVersionGreater(firstSupportedVersion, version)
|
||||
const upperBound = isMajorVersionGreater(version, lastSupportedVersion)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const getJoinLink = () => {
|
|||
|
||||
const copyJoinLink = async () => {
|
||||
miscUiState.wanOpened = true
|
||||
const joinLink = getJoinLink()
|
||||
const joinLink = getJoinLink()!
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(joinLink)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
//@ts-check
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
const { showModal, miscUiState } = require('../globalState')
|
||||
const { options, watchValue } = require('../optionsStorage')
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import PrismarineBlockLoader from 'prismarine-block'
|
|||
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState'
|
||||
import invspriteJson from './invsprite.json'
|
||||
import { options } from './optionsStorage'
|
||||
import { assertDefined } from './utils'
|
||||
|
||||
const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases
|
||||
const loadedImagesCache = new Map<string, HTMLImageElement>()
|
||||
|
|
@ -74,12 +75,13 @@ export const onGameLoad = (onLoad) => {
|
|||
text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Items: ${win.slots.map(slot => slot?.name).join(', ')}`
|
||||
})
|
||||
})
|
||||
bot.currentWindow['close']()
|
||||
bot.currentWindow?.['close']()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const findTextureInBlockStates = (name) => {
|
||||
assertDefined(viewer)
|
||||
const blockStates: BlockStates = viewer.world.customBlockStatesData || viewer.world.downloadedBlockStatesData
|
||||
const vars = blockStates[name]?.variants
|
||||
if (!vars) return
|
||||
|
|
@ -92,7 +94,7 @@ const findTextureInBlockStates = (name) => {
|
|||
}
|
||||
|
||||
const svSuToCoordinates = (path: string, u, v, su, sv = su) => {
|
||||
const img = getImage({ path })
|
||||
const img = getImage({ path })!
|
||||
if (!img.width) throw new Error(`Image ${path} is not loaded`)
|
||||
return [u * img.width, v * img.height, su * img.width, sv * img.height]
|
||||
}
|
||||
|
|
@ -130,6 +132,7 @@ const getInvspriteSlice = (name) => {
|
|||
}
|
||||
|
||||
const getImageSrc = (path): string | HTMLImageElement => {
|
||||
assertDefined(viewer)
|
||||
switch (path) {
|
||||
case 'gui/container/inventory': return InventoryGui
|
||||
case 'blocks': return viewer.world.customTexturesDataUrl || viewer.world.downloadedTextureImage
|
||||
|
|
@ -145,8 +148,9 @@ const getImageSrc = (path): string | HTMLImageElement => {
|
|||
return Dirt
|
||||
}
|
||||
|
||||
const getImage = ({ path = undefined, texture = undefined, blockData = undefined }, onLoad = () => {}) => {
|
||||
const loadPath = blockData ? 'blocks' : path ?? texture
|
||||
const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any }, onLoad = () => {}) => {
|
||||
if (!path && !texture) throw new Error('Either pass path or texture')
|
||||
const loadPath = (blockData ? 'blocks' : path ?? texture)!
|
||||
if (loadedImagesCache.has(loadPath)) {
|
||||
onLoad()
|
||||
} else {
|
||||
|
|
@ -184,7 +188,7 @@ const isFullBlock = (block: string) => {
|
|||
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
||||
}
|
||||
|
||||
const renderSlot = (slot: import('prismarine-item').Item, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } => {
|
||||
const renderSlot = (slot: import('prismarine-item').Item, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } | undefined => {
|
||||
const itemName = slot.name
|
||||
const isItem = loadedData.itemsByName[itemName]
|
||||
const fullBlock = isFullBlock(itemName)
|
||||
|
|
@ -230,7 +234,7 @@ export const renderSlotExternal = (slot) => {
|
|||
const data = renderSlot(slot, true)
|
||||
if (!data) return
|
||||
return {
|
||||
imageDataUrl: data.texture === 'invsprite' ? undefined : getImage({ path: data.texture }).src,
|
||||
imageDataUrl: data.texture === 'invsprite' ? undefined : getImage({ path: data.texture })?.src,
|
||||
sprite: data.slice && data.texture !== 'invsprite' ? data.slice.map(x => x * 2) : data.slice
|
||||
}
|
||||
}
|
||||
|
|
@ -238,7 +242,7 @@ export const renderSlotExternal = (slot) => {
|
|||
const upInventory = (inventory: boolean) => {
|
||||
// inv.pwindow.inv.slots[2].displayName = 'test'
|
||||
// inv.pwindow.inv.slots[2].blockData = getBlockData('dirt')
|
||||
const updateSlots = (inventory ? bot.inventory : bot.currentWindow).slots.map(slot => {
|
||||
const updateSlots = (inventory ? bot.inventory : bot.currentWindow)!.slots.map(slot => {
|
||||
// todo stateid
|
||||
if (!slot) return
|
||||
|
||||
|
|
@ -277,7 +281,7 @@ const implementedContainersGuiMap = {
|
|||
const openWindow = (type: string | undefined) => {
|
||||
// if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) {
|
||||
if (activeModalStack.length) { // game is not in foreground, don't close current modal
|
||||
if (type) bot.currentWindow['close']()
|
||||
if (type) bot.currentWindow?.['close']()
|
||||
return
|
||||
}
|
||||
showModal({
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import styles from './appStatus.module.css'
|
|||
import Button from './Button'
|
||||
import Screen from './Screen'
|
||||
|
||||
export default ({ status, isError, hideDots = false, lastStatus = '', backAction = undefined, actionsSlot = undefined }) => {
|
||||
export default ({ status, isError, hideDots = false, lastStatus = '', backAction = undefined as undefined | (() => void), actionsSlot = undefined }) => {
|
||||
const [loadingDots, setLoadingDots] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ export default () => {
|
|||
useDidUpdateEffect(() => {
|
||||
// todo play effect only when world successfully loaded
|
||||
if (!isOpen) {
|
||||
const divingElem: HTMLElement = document.querySelector('#viewer-canvas')
|
||||
const divingElem: HTMLElement = document.querySelector('#viewer-canvas')!
|
||||
divingElem.style.animationName = 'dive-animation'
|
||||
divingElem.parentElement.style.perspective = '1200px'
|
||||
divingElem.parentElement!.style.perspective = '1200px'
|
||||
divingElem.onanimationend = () => {
|
||||
divingElem.parentElement.style.perspective = ''
|
||||
divingElem.parentElement!.style.perspective = ''
|
||||
divingElem.onanimationend = null
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ export default () => {
|
|||
appStatusState.isError = false
|
||||
resetState()
|
||||
miscUiState.gameLoaded = false
|
||||
miscUiState.loadedDataVersion = undefined
|
||||
miscUiState.loadedDataVersion = null
|
||||
window.loadedData = undefined
|
||||
if (activeModalStacks['main-menu']) {
|
||||
insertActiveModalStack('main-menu')
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ void loadSound('button_click.mp3')
|
|||
export default forwardRef<HTMLButtonElement, Props>(({ label, icon, children, inScreen, ...args }, ref) => {
|
||||
const onClick = (e) => {
|
||||
void playSound('button_click.mp3')
|
||||
args.onClick(e)
|
||||
args.onClick?.(e)
|
||||
}
|
||||
if (inScreen) {
|
||||
args.style ??= {}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default ({ initialTooltip, ...args }: Props) => {
|
|||
useEffect(() => {
|
||||
let timeout
|
||||
function hide () {
|
||||
if (!localStorageKey) return
|
||||
localStorage[localStorageKey] = 'false'
|
||||
setShowTooltips(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
|
|||
useEffect(() => {
|
||||
creatingWorldState.version = defaultVersion
|
||||
void navigator.storage.estimate().then(({ quota, usage }) => {
|
||||
setQuota(`Storage usage: ${filesize(usage)} / ${filesize(quota)}`)
|
||||
setQuota(`Storage usage: ${usage === undefined ? '?' : filesize(usage)} / ${quota ? filesize(quota) : '?'}`)
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import { openURL } from '../menus/components/common'
|
||||
import { haveDirectoryPicker } from '../utils'
|
||||
import styles from './mainMenu.module.css'
|
||||
import Button from './Button'
|
||||
import ButtonWithTooltip from './ButtonWithTooltip'
|
||||
|
|
@ -18,7 +19,7 @@ interface Props {
|
|||
|
||||
const refreshApp = async () => {
|
||||
const registration = await navigator.serviceWorker.getRegistration()
|
||||
await registration.unregister()
|
||||
await registration?.unregister()
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +82,7 @@ export default ({ connectToServerAction, mapsProvider, singleplayerAction, optio
|
|||
icon='pixelarticons:folder'
|
||||
onClick={openFileAction}
|
||||
initialTooltip={{
|
||||
content: 'Load any 1.8-1.16 Java world' + (window.showDirectoryPicker ? '' : ' (zip)'),
|
||||
content: 'Load any 1.8-1.16 Java world' + (haveDirectoryPicker() ? '' : ' (zip)'),
|
||||
placement: 'bottom-start',
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export default () => {
|
|||
}
|
||||
showModal({ reactType: 'singleplayer' })
|
||||
}}
|
||||
githubAction={() => openURL(process.env.GITHUB_URL)}
|
||||
githubAction={() => openURL(process.env.GITHUB_URL!)}
|
||||
optionsAction={() => openOptionsMenu('main')}
|
||||
discordAction={() => openURL('https://discord.gg/4Ucm684Fq3')}
|
||||
openFileAction={e => {
|
||||
|
|
|
|||
|
|
@ -30,12 +30,12 @@ export type OptionMeta = GeneralItem & ({
|
|||
})
|
||||
|
||||
export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'toggle' }> }) => {
|
||||
const optionValue = useSnapshot(options)[item.id]
|
||||
const optionValue = useSnapshot(options)[item.id!]
|
||||
|
||||
return <Button
|
||||
label={`${item.text}: ${optionValue ? 'ON' : 'OFF'}`}
|
||||
onClick={() => {
|
||||
options[item.id] = !options[item.id]
|
||||
options[item.id!] = !options[item.id!]
|
||||
}}
|
||||
title={item.disabledReason ? `${item.disabledReason} | ${item.tooltip}` : item.tooltip}
|
||||
disabled={!!item.disabledReason}
|
||||
|
|
@ -46,15 +46,15 @@ export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'togg
|
|||
}
|
||||
|
||||
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
|
||||
const optionValue = useSnapshot(options)[item.id]
|
||||
const optionValue = useSnapshot(options)[item.id!]
|
||||
|
||||
const valueDisplay = useMemo(() => {
|
||||
if (item.valueText) return item.valueText(optionValue)
|
||||
return undefined // default display
|
||||
}, [optionValue])
|
||||
|
||||
return <Slider label={item.text} value={options[item.id]} min={item.min} max={item.max} updateValue={(value) => {
|
||||
options[item.id] = value
|
||||
return <Slider label={item.text!} value={options[item.id!]} min={item.min} max={item.max} updateValue={(value) => {
|
||||
options[item.id!] = value
|
||||
}} unit={item.unit} valueDisplay={valueDisplay} updateOnDragEnd={item.delayApply} />
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
|
|||
return formatter.format(-minutes, 'minute')
|
||||
}, [lastPlayed])
|
||||
const sizeFormatted = useMemo(() => {
|
||||
if (!size) return
|
||||
return filesize(size)
|
||||
}, [size])
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
|
|||
e.preventDefault()
|
||||
onInteraction?.(e.code === 'Enter' ? 'enter' : 'space')
|
||||
}
|
||||
}} onDoubleClick={() => onInteraction('enter')}>
|
||||
}} onDoubleClick={() => onInteraction?.('enter')}>
|
||||
<img className={styles.world_image} src={missingWorldPreview} />
|
||||
<div className={styles.world_info}>
|
||||
<div className={styles.world_title} title='level.dat world name'>{title}</div>
|
||||
|
|
@ -61,7 +62,7 @@ interface Props {
|
|||
|
||||
export default ({ worldData, onGeneralAction, onWorldAction }: Props) => {
|
||||
const containerRef = useRef<any>()
|
||||
const firstButton = useRef<HTMLButtonElement>()
|
||||
const firstButton = useRef<HTMLButtonElement>(null!)
|
||||
|
||||
useTypedEventListener(window, 'keydown', (e) => {
|
||||
if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useEffect } from 'react'
|
|||
import { fsState, loadSave, longArrayToNumber, readLevelDat } from '../loadSave'
|
||||
import { mountExportFolder, removeFileRecursiveAsync } from '../browserfs'
|
||||
import { hideCurrentModal, showModal } from '../globalState'
|
||||
import { setLoadingScreenStatus } from '../utils'
|
||||
import { haveDirectoryPicker, setLoadingScreenStatus } from '../utils'
|
||||
import { exportWorld } from '../builtinCommands'
|
||||
import Singleplayer, { WorldProps } from './Singleplayer'
|
||||
import { useIsModalActive } from './utils'
|
||||
|
|
@ -17,7 +17,7 @@ export const readWorlds = () => {
|
|||
try {
|
||||
const worlds = await fs.promises.readdir(`/data/worlds`)
|
||||
worldsProxy.value = (await Promise.allSettled(worlds.map(async (world) => {
|
||||
const { levelDat } = await readLevelDat(`/data/worlds/${world}`)
|
||||
const { levelDat } = (await readLevelDat(`/data/worlds/${world}`))!
|
||||
let size = 0
|
||||
// todo use whole dir size
|
||||
for (const region of await fs.promises.readdir(`/data/worlds/${world}/region`)) {
|
||||
|
|
@ -28,7 +28,7 @@ export const readWorlds = () => {
|
|||
name: world,
|
||||
title: levelDat.LevelName,
|
||||
lastPlayed: levelDat.LastPlayed && longArrayToNumber(levelDat.LastPlayed),
|
||||
detail: `${levelDat.Version.Name ?? 'unknown version'}, ${world}`,
|
||||
detail: `${levelDat.Version?.Name ?? 'unknown version'}, ${world}`,
|
||||
size,
|
||||
} satisfies WorldProps
|
||||
}))).filter(x => {
|
||||
|
|
@ -81,7 +81,7 @@ export default () => {
|
|||
}
|
||||
if (action === 'export') {
|
||||
const selectedVariant =
|
||||
window.showDirectoryPicker
|
||||
haveDirectoryPicker()
|
||||
? await showOptionsModal('Select export type', ['Select folder (recommended)', 'Download ZIP file'])
|
||||
: await showOptionsModal('Select export type', ['Download ZIP file'])
|
||||
if (!selectedVariant) return
|
||||
|
|
|
|||
25
src/utils.ts
25
src/utils.ts
|
|
@ -67,20 +67,20 @@ window.getScreenRefreshRate = getScreenRefreshRate
|
|||
* Allows to obtain the estimated Hz of the primary monitor in the system.
|
||||
*/
|
||||
export async function getScreenRefreshRate (): Promise<number> {
|
||||
let requestId = null
|
||||
let requestId = null as number | null
|
||||
let callbackTriggered = false
|
||||
let resolve
|
||||
|
||||
const DOMHighResTimeStampCollection = []
|
||||
const DOMHighResTimeStampCollection = [] as number[]
|
||||
|
||||
const triggerAnimation = (DOMHighResTimeStamp) => {
|
||||
DOMHighResTimeStampCollection.unshift(DOMHighResTimeStamp)
|
||||
|
||||
if (DOMHighResTimeStampCollection.length > 10) {
|
||||
const t0 = DOMHighResTimeStampCollection.pop()
|
||||
const t0 = DOMHighResTimeStampCollection.pop()!
|
||||
const fps = Math.floor(1000 * 10 / (DOMHighResTimeStamp - t0))
|
||||
|
||||
if (!callbackTriggered) {
|
||||
if (!callbackTriggered || fps > 1000) {
|
||||
resolve(Math.max(fps, 1000)/* , DOMHighResTimeStampCollection */)
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ export async function getScreenRefreshRate (): Promise<number> {
|
|||
window.requestAnimationFrame(triggerAnimation)
|
||||
|
||||
window.setTimeout(() => {
|
||||
window.cancelAnimationFrame(requestId)
|
||||
window.cancelAnimationFrame(requestId!)
|
||||
requestId = null
|
||||
}, 500)
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ export const isMajorVersionGreater = (ver1: string, ver2: string) => {
|
|||
return +a1 > +a2 || (+a1 === +a2 && +b1 > +b2)
|
||||
}
|
||||
|
||||
let ourLastStatus = ''
|
||||
let ourLastStatus: string | undefined = ''
|
||||
export const setLoadingScreenStatus = function (status: string | undefined | null, isError = false, hideDots = false, fromFlyingSquid = false) {
|
||||
// null can come from flying squid, should restore our last status
|
||||
if (status === null) {
|
||||
|
|
@ -168,6 +168,7 @@ export const toMajorVersion = (version) => {
|
|||
|
||||
let prevRenderDistance = options.renderDistance
|
||||
export const setRenderDistance = () => {
|
||||
assertDefined(worldView)
|
||||
worldView.viewDistance = options.renderDistance
|
||||
if (localServer) {
|
||||
localServer.players[0].emit('playerChangeRenderDistance', options.renderDistance)
|
||||
|
|
@ -182,14 +183,14 @@ export const reloadChunks = async () => {
|
|||
|
||||
export const openFilePicker = (specificCase?: 'resourcepack') => {
|
||||
// create and show input picker
|
||||
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')
|
||||
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')!
|
||||
if (!picker) {
|
||||
picker = document.createElement('input')
|
||||
picker.type = 'file'
|
||||
picker.accept = '.zip'
|
||||
|
||||
picker.addEventListener('change', () => {
|
||||
const file = picker.files[0]
|
||||
const file = picker.files?.[0]
|
||||
picker.value = ''
|
||||
if (!file) return
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
|
|
@ -217,3 +218,11 @@ export const resolveTimeout = async (promise, timeout = 10_000) => {
|
|||
}, timeout)
|
||||
})
|
||||
}
|
||||
|
||||
export function assertDefined<T> (x: T | undefined): asserts x is T {
|
||||
if (!x) throw new Error('Assertion failed. Something is not available')
|
||||
}
|
||||
|
||||
export const haveDirectoryPicker = () => {
|
||||
return !!window.showDirectoryPicker
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ watchValue(options, o => {
|
|||
|
||||
export const watchOptionsAfterViewerInit = () => {
|
||||
watchValue(options, o => {
|
||||
if (!viewer) return
|
||||
viewer.world.showChunkBorders = o.showChunkBorders
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import destroyStage9 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/de
|
|||
|
||||
import { Vec3 } from 'vec3'
|
||||
import { isGameActive } from './globalState'
|
||||
import { assertDefined } from './utils'
|
||||
|
||||
function getViewDirection (pitch, yaw) {
|
||||
const csPitch = Math.cos(pitch)
|
||||
|
|
@ -30,8 +31,19 @@ class WorldInteraction {
|
|||
prevBreakState
|
||||
currentDigTime
|
||||
prevOnGround
|
||||
/** @type {number} */
|
||||
lastBlockPlaced
|
||||
buttons = [false, false, false]
|
||||
lastButtons = [false, false, false]
|
||||
/** @type {number | undefined} */
|
||||
breakStartTime = 0
|
||||
/** @type {import('prismarine-block').Block | null} */
|
||||
cursorBlock = null
|
||||
/** @type {THREE.Mesh} */
|
||||
blockBreakMesh
|
||||
|
||||
init () {
|
||||
assertDefined(viewer)
|
||||
bot.on('physicsTick', () => { if (this.lastBlockPlaced < 4) this.lastBlockPlaced++ })
|
||||
bot.on('diggingCompleted', () => {
|
||||
this.breakStartTime = undefined
|
||||
|
|
@ -40,12 +52,6 @@ class WorldInteraction {
|
|||
this.breakStartTime = undefined
|
||||
})
|
||||
|
||||
// Init state
|
||||
this.buttons = [false, false, false]
|
||||
this.lastButtons = [false, false, false]
|
||||
this.breakStartTime = 0
|
||||
this.cursorBlock = null
|
||||
|
||||
const loader = new THREE.TextureLoader()
|
||||
this.breakTextures = []
|
||||
const destroyStagesImages = [
|
||||
|
|
@ -114,16 +120,21 @@ class WorldInteraction {
|
|||
})
|
||||
}
|
||||
|
||||
updateBlockInteractionLines (/** @type {Vec3 | null} */blockPos, /** @type {{position, width, height, depth}[]} */shapePositions = undefined) {
|
||||
updateBlockInteractionLines (/** @type {Vec3 | null} */blockPos, /** @type {{position, width, height, depth}[] | undefined} */shapePositions = undefined) {
|
||||
assertDefined(viewer)
|
||||
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
|
||||
return
|
||||
}
|
||||
if (this.interactionLines !== null) {
|
||||
viewer.scene.remove(this.interactionLines.mesh)
|
||||
this.interactionLines = null
|
||||
}
|
||||
if (blockPos === null || (this.interactionLines && blockPos.equals(this.interactionLines.blockPos))) {
|
||||
if (blockPos === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
//@ts-expect-error
|
||||
for (const { position, width, height, depth } of shapePositions) {
|
||||
const geometry = new THREE.BoxGeometry(1.001 * width, 1.001 * height, 1.001 * depth)
|
||||
const mesh = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), new THREE.LineBasicMaterial({ color: 0 }))
|
||||
|
|
@ -139,7 +150,7 @@ class WorldInteraction {
|
|||
update () {
|
||||
const cursorBlock = bot.blockAtCursor(5)
|
||||
let cursorBlockDiggable = cursorBlock
|
||||
if (!bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
|
||||
if (cursorBlock && !bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
|
||||
|
||||
let cursorChanged = !cursorBlock !== !this.cursorBlock
|
||||
if (cursorBlock && this.cursorBlock) {
|
||||
|
|
@ -178,9 +189,9 @@ class WorldInteraction {
|
|||
&& (!this.lastButtons[0] || (cursorChanged && Date.now() - (this.lastDigged ?? 0) > 100) || onGround !== this.prevOnGround)
|
||||
&& onGround
|
||||
) {
|
||||
this.currentDigTime = bot.digTime(cursorBlock)
|
||||
this.currentDigTime = bot.digTime(cursorBlockDiggable)
|
||||
this.breakStartTime = performance.now()
|
||||
bot.dig(cursorBlock, 'ignore').catch((err) => {
|
||||
bot.dig(cursorBlockDiggable, 'ignore').catch((err) => {
|
||||
if (err.message === 'Digging aborted') return
|
||||
throw err
|
||||
})
|
||||
|
|
@ -216,16 +227,18 @@ class WorldInteraction {
|
|||
}
|
||||
|
||||
// Show break animation
|
||||
if (this.breakStartTime && bot.game.gameMode !== 'creative') {
|
||||
if (cursorBlockDiggable && this.breakStartTime && bot.game.gameMode !== 'creative') {
|
||||
const elapsed = performance.now() - this.breakStartTime
|
||||
const time = bot.digTime(cursorBlock)
|
||||
const time = bot.digTime(cursorBlockDiggable)
|
||||
if (time !== this.currentDigTime) {
|
||||
console.warn('dig time changed! cancelling!', time, 'from', this.currentDigTime) // todo
|
||||
try { bot.stopDigging() } catch { }
|
||||
}
|
||||
const state = Math.floor((elapsed / time) * 10)
|
||||
//@ts-expect-error
|
||||
this.blockBreakMesh.material.map = this.breakTextures[state] ?? this.breakTextures.at(-1)
|
||||
if (state !== this.prevBreakState) {
|
||||
//@ts-expect-error
|
||||
this.blockBreakMesh.material.needsUpdate = true
|
||||
}
|
||||
this.prevBreakState = state
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
"skipLibCheck": true,
|
||||
// this the only options that allows smooth transition from js to ts (by not dropping types from js files)
|
||||
// however might need to consider includeing *only needed libraries* instead of using this
|
||||
"maxNodeModuleJsDepth": 1
|
||||
// "strictNullChecks": true
|
||||
"maxNodeModuleJsDepth": 1,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue