feat: rework hand! enable by default, fix bow anim (#261)
* refactor swing animation to controller * idle animator!!!! * implelment state switch transition * a huge fix for UI server edit! * adjust ui scaling so main menu elements clip less * view bobbing, new config name, ws: * EXTREMELY important fixes to entities rendering * a lot of fixes, add dns resolve fallback * improve f3 E, fix modal not found edge case * set correctly target for old browsers, should fix ios 14 crash * unecessary big refactor, to fix ts err * fix isWysiwyg check * fix entities rendering count
This commit is contained in:
parent
0f29053ca6
commit
65af9a73c2
45 changed files with 2141 additions and 556 deletions
28
package.json
28
package.json
|
|
@ -63,6 +63,7 @@
|
|||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.4",
|
||||
"deepslate": "^0.23.5",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"eruda": "^3.0.1",
|
||||
"esbuild": "^0.19.3",
|
||||
|
|
@ -144,7 +145,7 @@
|
|||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.31",
|
||||
"mc-assets": "^0.2.34",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
|
|
@ -168,15 +169,22 @@
|
|||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"systeminformation": "^5.21.22"
|
||||
},
|
||||
"browserslist": [
|
||||
"iOS >= 14",
|
||||
"Android >= 13",
|
||||
"Chrome >= 103",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"> 0.5%"
|
||||
],
|
||||
"browserslist": {
|
||||
"production": [
|
||||
"iOS >= 14",
|
||||
"Android >= 13",
|
||||
"Chrome >= 103",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"> 0.5%"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"buffer": "^6.0.3",
|
||||
|
|
|
|||
72
pnpm-lock.yaml
generated
72
pnpm-lock.yaml
generated
|
|
@ -97,6 +97,9 @@ importers:
|
|||
debug:
|
||||
specifier: ^4.3.4
|
||||
version: 4.4.0(supports-color@8.1.1)
|
||||
deepslate:
|
||||
specifier: ^0.23.5
|
||||
version: 0.23.5
|
||||
diff-match-patch:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5
|
||||
|
|
@ -132,7 +135,7 @@ importers:
|
|||
version: 4.17.21
|
||||
mcraft-fun-mineflayer:
|
||||
specifier: 0.0.3
|
||||
version: 0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13))
|
||||
version: 0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13))
|
||||
minecraft-data:
|
||||
specifier: 3.83.1
|
||||
version: 3.83.1
|
||||
|
|
@ -346,14 +349,14 @@ importers:
|
|||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
mc-assets:
|
||||
specifier: ^0.2.31
|
||||
version: 0.2.31
|
||||
specifier: ^0.2.34
|
||||
version: 0.2.34
|
||||
minecraft-inventory-gui:
|
||||
specifier: github:zardoy/minecraft-inventory-gui#next
|
||||
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/75e940a4cd50d89e0ba03db3733d5d704917a3c8(@types/react@18.2.20)(react@18.2.0)
|
||||
mineflayer:
|
||||
specifier: github:zardoy/mineflayer
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13)
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13)
|
||||
mineflayer-pathfinder:
|
||||
specifier: ^2.4.4
|
||||
version: 2.4.4
|
||||
|
|
@ -3791,6 +3794,9 @@ packages:
|
|||
character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
|
||||
charenc@0.0.2:
|
||||
resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==}
|
||||
|
||||
check-error@1.0.3:
|
||||
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
||||
|
||||
|
|
@ -4065,6 +4071,9 @@ packages:
|
|||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
crypt@0.0.2:
|
||||
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
|
||||
|
||||
crypto-browserify@3.12.0:
|
||||
resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==}
|
||||
|
||||
|
|
@ -4218,6 +4227,9 @@ packages:
|
|||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
deepslate@0.23.5:
|
||||
resolution: {integrity: sha512-FjBBbuPUI1Y/dXtUc4WiCJSA7s7yRAXepD7qWRF6wX5m/q7AVRauMEShu8lphRvqCtJyxcYFZmISwX5OOH/tWw==}
|
||||
|
||||
default-browser-id@3.0.0:
|
||||
resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -5183,6 +5195,9 @@ packages:
|
|||
github-slugger@1.5.0:
|
||||
resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==}
|
||||
|
||||
gl-matrix@3.4.3:
|
||||
resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
|
||||
|
||||
gl@6.0.2:
|
||||
resolution: {integrity: sha512-yBbfpChOtFvg5D+KtMaBFvj6yt3vUnheNAH+UrQH2TfDB8kr0tERdL0Tjhe0W7xJ6jR6ftQBluTZR9jXUnKe8g==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -6228,8 +6243,8 @@ packages:
|
|||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mc-assets@0.2.31:
|
||||
resolution: {integrity: sha512-+WAU9+iRbBgWWgsrQ52j0Xqq+Qs7UbjlVtZq1NCTqpIAvaEKNCwM7u2qFV5hoj4RpBAsVmkgLissdjLddE8WaA==}
|
||||
mc-assets@0.2.34:
|
||||
resolution: {integrity: sha512-BvE2mVs9XETLFb+FN1Zbc4mJ+CvZqgxVd3kxhERp1QljudDmMsWsMcK2EUTrevuE6a7L3F2kx8XC1vVA79i/ow==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
mcraft-fun-mineflayer@0.0.3:
|
||||
|
|
@ -6251,6 +6266,9 @@ packages:
|
|||
md5.js@1.3.5:
|
||||
resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==}
|
||||
|
||||
md5@2.3.0:
|
||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||
|
||||
mdast-util-definitions@4.0.0:
|
||||
resolution: {integrity: sha512-k8AJ6aNnUkB7IE+5azR9h81O5EQ/cTDXtWdMq9Kk5KcEW/8ritU5CeLg/9HhOC++nALHBlaogJ5jz0Ybk3kPMQ==}
|
||||
|
||||
|
|
@ -6462,8 +6480,8 @@ packages:
|
|||
resolution: {integrity: sha512-q7cmpZFaSI6sodcMJxc2GkV8IO84HbsUP+xNipGKfGg+FMISKabzdJ838Axb60qRtZrp6ny7LluQE7lesHvvxQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5}
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828}
|
||||
version: 4.25.0
|
||||
engines: {node: '>=18'}
|
||||
|
||||
|
|
@ -6874,6 +6892,9 @@ packages:
|
|||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
pako@2.1.0:
|
||||
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
|
||||
|
||||
param-case@3.0.4:
|
||||
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
|
||||
|
||||
|
|
@ -9258,6 +9279,9 @@ packages:
|
|||
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
zod@3.24.1:
|
||||
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
|
||||
|
||||
zustand@3.6.5:
|
||||
resolution: {integrity: sha512-/WfLJuXiEJimt61KGMHebrFBwckkCHGhAgVXTgPQHl6IMzjqm6MREb1OnDSnCRiSmRdhgdFCctceg6tSm79hiw==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
|
|
@ -13604,6 +13628,8 @@ snapshots:
|
|||
|
||||
character-entities@2.0.2: {}
|
||||
|
||||
charenc@0.0.2: {}
|
||||
|
||||
check-error@1.0.3:
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
|
|
@ -13924,6 +13950,8 @@ snapshots:
|
|||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
crypt@0.0.2: {}
|
||||
|
||||
crypto-browserify@3.12.0:
|
||||
dependencies:
|
||||
browserify-cipher: 1.0.1
|
||||
|
|
@ -14125,6 +14153,12 @@ snapshots:
|
|||
|
||||
deepmerge@4.3.1: {}
|
||||
|
||||
deepslate@0.23.5:
|
||||
dependencies:
|
||||
gl-matrix: 3.4.3
|
||||
md5: 2.3.0
|
||||
pako: 2.1.0
|
||||
|
||||
default-browser-id@3.0.0:
|
||||
dependencies:
|
||||
bplist-parser: 0.2.0
|
||||
|
|
@ -15462,6 +15496,8 @@ snapshots:
|
|||
|
||||
github-slugger@1.5.0: {}
|
||||
|
||||
gl-matrix@3.4.3: {}
|
||||
|
||||
gl@6.0.2:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
|
|
@ -16643,14 +16679,16 @@ snapshots:
|
|||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mc-assets@0.2.31: {}
|
||||
mc-assets@0.2.34:
|
||||
dependencies:
|
||||
zod: 3.24.1
|
||||
|
||||
mcraft-fun-mineflayer@0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13)):
|
||||
mcraft-fun-mineflayer@0.0.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13)):
|
||||
dependencies:
|
||||
'@zardoy/flying-squid': 0.0.49(encoding@0.1.13)
|
||||
exit-hook: 2.2.1
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13)
|
||||
prismarine-item: 1.16.0
|
||||
ws: 8.18.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -16667,6 +16705,12 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
md5@2.3.0:
|
||||
dependencies:
|
||||
charenc: 0.0.2
|
||||
crypt: 0.0.2
|
||||
is-buffer: 1.1.6
|
||||
|
||||
mdast-util-definitions@4.0.0:
|
||||
dependencies:
|
||||
unist-util-visit: 2.0.3
|
||||
|
|
@ -17042,7 +17086,7 @@ snapshots:
|
|||
- encoding
|
||||
- supports-color
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/54f8c2282d822ad02967a197bda36302a4e7b4a5(encoding@0.1.13):
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/4fa1af9964cab91315d8d1ae02615f3039638828(encoding@0.1.13):
|
||||
dependencies:
|
||||
minecraft-data: 3.83.1
|
||||
minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/e9eb551ba30ec2e742c49e6927be6402b413bb76(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13)
|
||||
|
|
@ -17556,6 +17600,8 @@ snapshots:
|
|||
|
||||
pako@1.0.11: {}
|
||||
|
||||
pako@2.1.0: {}
|
||||
|
||||
param-case@3.0.4:
|
||||
dependencies:
|
||||
dot-case: 3.0.4
|
||||
|
|
@ -20407,6 +20453,8 @@ snapshots:
|
|||
|
||||
yocto-queue@1.0.0: {}
|
||||
|
||||
zod@3.24.1: {}
|
||||
|
||||
zustand@3.6.5(react@18.2.0):
|
||||
optionalDependencies:
|
||||
react: 18.2.0
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const buildOptions = {
|
|||
'debugger'
|
||||
] : [],
|
||||
sourcemap: 'linked',
|
||||
target: watch ? undefined : ['ios14'],
|
||||
write: false,
|
||||
metafile: true,
|
||||
outdir: path.join(__dirname, './dist'),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { defineConfig, mergeRsbuildConfig } from '@rsbuild/core';
|
||||
import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core';
|
||||
import supportedVersions from '../src/supportedVersions.mjs'
|
||||
import childProcess from 'child_process'
|
||||
import path, { dirname, join } from 'path'
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||
import fs from 'fs'
|
||||
import fsExtra from 'fs-extra'
|
||||
import { appAndRendererSharedConfig, rspackViewerConfig } from './rsbuildSharedConfig';
|
||||
|
||||
const mcDataPath = join(__dirname, '../generated/minecraft-data-optimized.json')
|
||||
|
||||
// if (!fs.existsSync('./playground/textures')) {
|
||||
// fsExtra.copySync('node_modules/mc-assets/dist/other-textures/latest/entity', './playground/textures/entity')
|
||||
// }
|
||||
|
||||
if (!fs.existsSync(mcDataPath)) {
|
||||
childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: path.join(__dirname, '..') })
|
||||
}
|
||||
|
|
@ -36,5 +41,19 @@ export default mergeRsbuildConfig(
|
|||
'globalThis.includedVersions': JSON.stringify(supportedVersions),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'test',
|
||||
setup (build: RsbuildPluginAPI) {
|
||||
const prep = async () => {
|
||||
fsExtra.copySync(join(__dirname, '../node_modules/mc-assets/dist/other-textures/latest/entity'), join(__dirname, './dist/textures/entity'))
|
||||
}
|
||||
build.onBeforeBuild(async () => {
|
||||
await prep()
|
||||
})
|
||||
build.onBeforeStartDevServer(() => prep())
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
|
|
|||
174
renderer/viewer/lib/DebugGui.ts
Normal file
174
renderer/viewer/lib/DebugGui.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// eslint-disable-next-line import/no-named-as-default
|
||||
import GUI from 'lil-gui'
|
||||
|
||||
export interface ParamMeta {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
}
|
||||
|
||||
export class DebugGui {
|
||||
private gui: GUI
|
||||
private readonly storageKey: string
|
||||
private target: any
|
||||
private readonly params: string[]
|
||||
private readonly paramsMeta: Record<string, ParamMeta>
|
||||
private _visible = false // Default to not visible
|
||||
private readonly initialValues: Record<string, any> = {} // Store initial values
|
||||
private initialized = false
|
||||
|
||||
constructor (id: string, target: any, params?: string[], paramsMeta?: Record<string, ParamMeta>) {
|
||||
this.gui = new GUI()
|
||||
this.storageKey = `debug_params_${id}`
|
||||
this.target = target
|
||||
this.paramsMeta = paramsMeta ?? {}
|
||||
this.params = params ?? Object.keys(target)
|
||||
|
||||
// Store initial values
|
||||
for (const param of this.params) {
|
||||
this.initialValues[param] = target[param]
|
||||
}
|
||||
|
||||
// Hide by default
|
||||
this.gui.domElement.style.display = 'none'
|
||||
}
|
||||
|
||||
// Initialize and show the GUI
|
||||
activate () {
|
||||
if (!this.initialized) {
|
||||
this.loadSavedValues()
|
||||
this.setupControls()
|
||||
this.initialized = true
|
||||
}
|
||||
this.show()
|
||||
return this
|
||||
}
|
||||
|
||||
// Getter for visibility
|
||||
get visible (): boolean {
|
||||
return this._visible
|
||||
}
|
||||
|
||||
// Setter for visibility
|
||||
set visible (value: boolean) {
|
||||
this._visible = value
|
||||
this.gui.domElement.style.display = value ? 'block' : 'none'
|
||||
this.saveVisibility()
|
||||
}
|
||||
|
||||
private loadSavedValues () {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.storageKey)
|
||||
if (saved) {
|
||||
const values = JSON.parse(saved)
|
||||
// Apply saved values to target
|
||||
for (const param of this.params) {
|
||||
if (param in values) {
|
||||
const value = values[param]
|
||||
if (value !== null) {
|
||||
this.target[param] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load debug values:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private saveValues (deleteKey = false) {
|
||||
try {
|
||||
const values = {}
|
||||
for (const param of this.params) {
|
||||
values[param] = this.target[param]
|
||||
}
|
||||
if (deleteKey) {
|
||||
localStorage.removeItem(this.storageKey)
|
||||
} else {
|
||||
localStorage.setItem(this.storageKey, JSON.stringify(values))
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to save debug values:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private saveVisibility () {
|
||||
try {
|
||||
localStorage.setItem(`${this.storageKey}_visible`, this._visible.toString())
|
||||
} catch (e) {
|
||||
console.warn('Failed to save debug visibility:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private setupControls () {
|
||||
// Add visibility toggle at the top
|
||||
this.gui.add(this, 'visible').name('Show Controls')
|
||||
this.gui.add({ resetAll: () => {
|
||||
for (const param of this.params) {
|
||||
this.target[param] = this.initialValues[param]
|
||||
}
|
||||
this.saveValues(true)
|
||||
this.gui.destroy()
|
||||
this.gui = new GUI()
|
||||
this.setupControls()
|
||||
} }, 'resetAll').name('Reset All Parameters')
|
||||
|
||||
for (const param of this.params) {
|
||||
const value = this.target[param]
|
||||
const meta = this.paramsMeta[param] ?? {}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
// For numbers, use meta values or calculate reasonable defaults
|
||||
const min = meta.min ?? value - Math.abs(value * 2)
|
||||
const max = meta.max ?? value + Math.abs(value * 2)
|
||||
const step = meta.step ?? Math.abs(value) / 100
|
||||
|
||||
this.gui.add(this.target, param, min, max, step)
|
||||
.onChange(() => this.saveValues())
|
||||
} else if (typeof value === 'boolean') {
|
||||
// For booleans, create a checkbox
|
||||
this.gui.add(this.target, param)
|
||||
.onChange(() => this.saveValues())
|
||||
} else if (typeof value === 'string' && ['x', 'y', 'z'].includes(param)) {
|
||||
// Special case for xyz coordinates
|
||||
const min = meta.min ?? -10
|
||||
const max = meta.max ?? 10
|
||||
const step = meta.step ?? 0.1
|
||||
|
||||
this.gui.add(this.target, param, min, max, step)
|
||||
.onChange(() => this.saveValues())
|
||||
} else if (Array.isArray(value)) {
|
||||
// For arrays, create a dropdown
|
||||
this.gui.add(this.target, param, value)
|
||||
.onChange(() => this.saveValues())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method to manually trigger save
|
||||
save () {
|
||||
this.saveValues()
|
||||
this.saveVisibility()
|
||||
}
|
||||
|
||||
// Method to destroy the GUI and clean up
|
||||
destroy () {
|
||||
this.saveVisibility()
|
||||
this.gui.destroy()
|
||||
}
|
||||
|
||||
// Toggle visibility
|
||||
toggle () {
|
||||
this.visible = !this.visible
|
||||
}
|
||||
|
||||
// Show the GUI
|
||||
show () {
|
||||
this.visible = true
|
||||
}
|
||||
|
||||
// Hide the GUI
|
||||
hide () {
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
75
renderer/viewer/lib/animationController.ts
Normal file
75
renderer/viewer/lib/animationController.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
|
||||
export class AnimationController {
|
||||
private currentAnimation: tweenJs.Group | null = null
|
||||
private isAnimating = false
|
||||
private cancelRequested = false
|
||||
private completionCallbacks: Array<() => void> = []
|
||||
|
||||
/** Main method */
|
||||
async startAnimation (createAnimation: () => tweenJs.Group): Promise<void> {
|
||||
if (this.isAnimating) {
|
||||
await this.cancelCurrentAnimation()
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.isAnimating = true
|
||||
this.cancelRequested = false
|
||||
this.currentAnimation = createAnimation()
|
||||
|
||||
this.completionCallbacks.push(() => {
|
||||
this.isAnimating = false
|
||||
this.currentAnimation = null
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** Main method */
|
||||
async cancelCurrentAnimation (): Promise<void> {
|
||||
if (!this.isAnimating) return
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.cancelRequested = true
|
||||
this.completionCallbacks.push(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
animationCycleFinish () {
|
||||
if (this.cancelRequested) this.forceFinish()
|
||||
}
|
||||
|
||||
forceFinish () {
|
||||
if (!this.isAnimating) return
|
||||
|
||||
if (this.currentAnimation) {
|
||||
for (const tween of this.currentAnimation.getAll()) tween.stop()
|
||||
this.currentAnimation.removeAll()
|
||||
this.currentAnimation = null
|
||||
}
|
||||
|
||||
this.isAnimating = false
|
||||
this.cancelRequested = false
|
||||
|
||||
const callbacks = [...this.completionCallbacks]
|
||||
this.completionCallbacks = []
|
||||
for (const cb of callbacks) cb()
|
||||
}
|
||||
|
||||
/** Required method */
|
||||
update () {
|
||||
if (this.currentAnimation) {
|
||||
this.currentAnimation.update()
|
||||
}
|
||||
}
|
||||
|
||||
get isActive () {
|
||||
return this.isAnimating
|
||||
}
|
||||
|
||||
get shouldCancel () {
|
||||
return this.cancelRequested
|
||||
}
|
||||
}
|
||||
94
renderer/viewer/lib/basePlayerState.ts
Normal file
94
renderer/viewer/lib/basePlayerState.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { Vec3 } from 'vec3'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||
import { proxy } from 'valtio'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
|
||||
export type MovementState = 'NOT_MOVING' | 'WALKING' | 'SPRINTING' | 'SNEAKING'
|
||||
export type ItemSpecificContextProperties = Partial<Pick<ItemSelector['properties'], 'minecraft:using_item' | 'minecraft:use_duration' | 'minecraft:use_cycle' | 'minecraft:display_context'>>
|
||||
|
||||
|
||||
export type PlayerStateEvents = {
|
||||
heldItemChanged: (item: HandItemBlock | undefined, isLeftHand: boolean) => void
|
||||
}
|
||||
|
||||
export interface IPlayerState {
|
||||
getEyeHeight(): number
|
||||
getMovementState(): MovementState
|
||||
getVelocity(): Vec3
|
||||
isOnGround(): boolean
|
||||
isSneaking(): boolean
|
||||
isFlying(): boolean
|
||||
isSprinting (): boolean
|
||||
getItemUsageTicks?(): number
|
||||
// isUsingItem?(): boolean
|
||||
getHeldItem?(isLeftHand: boolean): HandItemBlock | undefined
|
||||
username?: string
|
||||
onlineMode?: boolean
|
||||
|
||||
events: TypedEmitter<PlayerStateEvents>
|
||||
|
||||
reactive: {
|
||||
playerSkin: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
export class BasePlayerState implements IPlayerState {
|
||||
getItemUsageTicks? (): number {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
getHeldItem? (isLeftHand: boolean): HandItemBlock | undefined {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
reactive = proxy({
|
||||
playerSkin: undefined
|
||||
})
|
||||
protected movementState: MovementState = 'NOT_MOVING'
|
||||
protected velocity = new Vec3(0, 0, 0)
|
||||
protected onGround = true
|
||||
protected sneaking = false
|
||||
protected flying = false
|
||||
protected sprinting = false
|
||||
readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents>
|
||||
|
||||
getEyeHeight (): number {
|
||||
return 1.62
|
||||
}
|
||||
|
||||
getMovementState (): MovementState {
|
||||
return this.movementState
|
||||
}
|
||||
|
||||
getVelocity (): Vec3 {
|
||||
return this.velocity
|
||||
}
|
||||
|
||||
isOnGround (): boolean {
|
||||
return this.onGround
|
||||
}
|
||||
|
||||
isSneaking (): boolean {
|
||||
return this.sneaking
|
||||
}
|
||||
|
||||
isFlying (): boolean {
|
||||
return this.flying
|
||||
}
|
||||
|
||||
isSprinting (): boolean {
|
||||
return this.sprinting
|
||||
}
|
||||
|
||||
// For testing purposes
|
||||
setState (state: Partial<{
|
||||
movementState: MovementState
|
||||
velocity: Vec3
|
||||
onGround: boolean
|
||||
sneaking: boolean
|
||||
flying: boolean
|
||||
sprinting: boolean
|
||||
}>) {
|
||||
Object.assign(this, state)
|
||||
}
|
||||
}
|
||||
94
renderer/viewer/lib/cameraBobbing.ts
Normal file
94
renderer/viewer/lib/cameraBobbing.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
export class CameraBobbing {
|
||||
private walkDistance = 0
|
||||
private prevWalkDistance = 0
|
||||
private bobAmount = 0
|
||||
private prevBobAmount = 0
|
||||
private readonly gameTimer = new GameTimer()
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
constructor (
|
||||
private readonly BOB_FREQUENCY: number = Math.PI, // How fast the bob cycles
|
||||
private readonly BOB_BASE_AMPLITUDE: number = 0.5, // Base amplitude of the bob
|
||||
private readonly VERTICAL_MULTIPLIER: number = 1, // Vertical movement multiplier
|
||||
private readonly ROTATION_MULTIPLIER_Z: number = 3, // Roll rotation multiplier
|
||||
private readonly ROTATION_MULTIPLIER_X: number = 5 // Pitch rotation multiplier
|
||||
) {}
|
||||
|
||||
// Call this when player is moving
|
||||
public updateWalkDistance (distance: number): void {
|
||||
this.prevWalkDistance = this.walkDistance
|
||||
this.walkDistance = distance
|
||||
}
|
||||
|
||||
// Call this when player is moving to update bob amount
|
||||
public updateBobAmount (isMoving: boolean): void {
|
||||
const targetBob = isMoving ? 1 : 0
|
||||
this.prevBobAmount = this.bobAmount
|
||||
|
||||
// Update timing
|
||||
const ticks = this.gameTimer.update()
|
||||
const deltaTime = ticks / 20 // Convert ticks to seconds assuming 20 TPS
|
||||
|
||||
// Smooth transition for bob amount
|
||||
const bobDelta = (targetBob - this.bobAmount) * Math.min(1, deltaTime * 10)
|
||||
this.bobAmount += bobDelta
|
||||
}
|
||||
|
||||
// Call this in your render/animation loop
|
||||
public getBobbing (): { position: { x: number, y: number }, rotation: { x: number, z: number } } {
|
||||
// Interpolate walk distance
|
||||
const walkDist = this.prevWalkDistance +
|
||||
(this.walkDistance - this.prevWalkDistance) * this.gameTimer.partialTick
|
||||
|
||||
// Interpolate bob amount
|
||||
const bob = this.prevBobAmount +
|
||||
(this.bobAmount - this.prevBobAmount) * this.gameTimer.partialTick
|
||||
|
||||
// Calculate total distance for bob cycle
|
||||
const totalDist = -(walkDist * this.BOB_FREQUENCY)
|
||||
|
||||
// Calculate offsets
|
||||
const xOffset = Math.sin(totalDist) * bob * this.BOB_BASE_AMPLITUDE
|
||||
const yOffset = -Math.abs(Math.cos(totalDist) * bob) * this.VERTICAL_MULTIPLIER
|
||||
|
||||
// Calculate rotations (in radians)
|
||||
const zRot = (Math.sin(totalDist) * bob * this.ROTATION_MULTIPLIER_Z) * (Math.PI / 180)
|
||||
const xRot = (Math.abs(Math.cos(totalDist - 0.2) * bob) * this.ROTATION_MULTIPLIER_X) * (Math.PI / 180)
|
||||
|
||||
return {
|
||||
position: { x: xOffset, y: yOffset },
|
||||
rotation: { x: xRot, z: zRot }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GameTimer {
|
||||
private readonly msPerTick: number
|
||||
private lastMs: number
|
||||
public partialTick = 0
|
||||
|
||||
constructor (tickRate = 20) {
|
||||
this.msPerTick = 1000 / tickRate
|
||||
this.lastMs = performance.now()
|
||||
}
|
||||
|
||||
update (): number {
|
||||
const currentMs = performance.now()
|
||||
const deltaSinceLastTick = currentMs - this.lastMs
|
||||
|
||||
// Calculate how much of a tick has passed
|
||||
const tickDelta = deltaSinceLastTick / this.msPerTick
|
||||
this.lastMs = currentMs
|
||||
|
||||
// Add to accumulated partial ticks
|
||||
this.partialTick += tickDelta
|
||||
|
||||
// Get whole number of ticks that should occur
|
||||
const wholeTicks = Math.floor(this.partialTick)
|
||||
|
||||
// Keep the remainder as the new partial tick
|
||||
this.partialTick -= wholeTicks
|
||||
|
||||
return wholeTicks
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import * as THREE from 'three'
|
|||
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
||||
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
|
||||
// todo replace with url
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { degreesToRadians } from '@nxg-org/mineflayer-tracker/lib/mathUtils'
|
||||
import { NameTagObject } from 'skinview3d/libs/nametag'
|
||||
import { flat, fromFormattedString } from '@xmcl/text-component'
|
||||
|
|
@ -23,6 +22,8 @@ import { disposeObject } from './threeJsUtils'
|
|||
import { armorModel, armorTextures } from './entity/armorModels'
|
||||
import { Viewer } from './viewer'
|
||||
import { getBlockMeshFromModel } from './holdingBlock'
|
||||
import { ItemSpecificContextProperties } from './basePlayerState'
|
||||
import { loadSkinImage, getLookupUrl, stevePngUrl, steveTexture } from './utils/skins'
|
||||
const { loadTexture } = globalThis.isElectron ? require('./utils.electron.js') : require('./utils')
|
||||
|
||||
export const TWEEN_DURATION = 120
|
||||
|
|
@ -217,7 +218,7 @@ export class Entities extends EventEmitter {
|
|||
itemsTexture: THREE.Texture | null = null
|
||||
cachedMapsImages = {} as Record<number, string>
|
||||
itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
|
||||
getItemUv: undefined | ((item: Record<string, any>) => {
|
||||
getItemUv: undefined | ((item: Record<string, any>, specificProps: ItemSpecificContextProperties) => {
|
||||
texture: THREE.Texture;
|
||||
u: number;
|
||||
v: number;
|
||||
|
|
@ -229,6 +230,20 @@ export class Entities extends EventEmitter {
|
|||
modelName: string
|
||||
})
|
||||
|
||||
get entitiesByName (): Record<string, SceneEntity[]> {
|
||||
const byName: Record<string, SceneEntity[]> = {}
|
||||
for (const entity of Object.values(this.entities)) {
|
||||
if (!entity['realName']) continue
|
||||
byName[entity['realName']] = byName[entity['realName']] || []
|
||||
byName[entity['realName']].push(entity)
|
||||
}
|
||||
return byName
|
||||
}
|
||||
|
||||
get entitiesRenderingCount (): number {
|
||||
return Object.values(this.entities).filter(entity => entity.visible).length
|
||||
}
|
||||
|
||||
constructor (public viewer: Viewer) {
|
||||
super()
|
||||
this.entitiesOptions = {}
|
||||
|
|
@ -289,12 +304,12 @@ export class Entities extends EventEmitter {
|
|||
const distanceSquared = dx * dx + dy * dy + dz * dz
|
||||
|
||||
// Get chunk coordinates
|
||||
const chunkX = Math.floor(entity.position.x / 16)
|
||||
const chunkZ = Math.floor(entity.position.z / 16)
|
||||
const chunkX = Math.floor(entity.position.x / 16) * 16
|
||||
const chunkZ = Math.floor(entity.position.z / 16) * 16
|
||||
const chunkKey = `${chunkX},${chunkZ}`
|
||||
|
||||
// Entity is visible if within 16 blocks OR in a finished chunk
|
||||
entity.visible = distanceSquared < VISIBLE_DISTANCE || this.viewer.world.finishedChunks[chunkKey]
|
||||
entity.visible = !!(distanceSquared < VISIBLE_DISTANCE || this.viewer.world.finishedChunks[chunkKey])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -304,12 +319,15 @@ export class Entities extends EventEmitter {
|
|||
return playerObject
|
||||
}
|
||||
|
||||
// fixme workaround
|
||||
defaultSteveTexture
|
||||
|
||||
uuidPerSkinUrlsCache = {} as Record<string, { skinUrl?: string, capeUrl?: string }>
|
||||
|
||||
// true means use default skin url
|
||||
private isCanvasBlank (canvas: HTMLCanvasElement): boolean {
|
||||
return !canvas.getContext('2d')
|
||||
?.getImageData(0, 0, canvas.width, canvas.height).data
|
||||
.some(channel => channel !== 0)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
updatePlayerSkin (entityId: string | number, username: string | undefined, uuid: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
||||
if (uuid) {
|
||||
if (typeof skinUrl === 'string' || typeof capeUrl === 'string') this.uuidPerSkinUrlsCache[uuid] = {}
|
||||
|
|
@ -321,38 +339,68 @@ export class Entities extends EventEmitter {
|
|||
capeUrl ??= this.uuidPerSkinUrlsCache[uuid]?.capeUrl
|
||||
}
|
||||
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
const playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
// const username = this.entities[entityId].username
|
||||
// or https://mulv.vercel.app/
|
||||
|
||||
if (skinUrl === true) {
|
||||
skinUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=skin`
|
||||
if (!username) return
|
||||
skinUrl = getLookupUrl(username, 'skin')
|
||||
}
|
||||
loadImage(skinUrl).then(image => {
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
/** @type {THREE.CanvasTexture} */
|
||||
let skinTexture
|
||||
if (skinUrl === stevePng && this.defaultSteveTexture) {
|
||||
skinTexture = this.defaultSteveTexture
|
||||
} else {
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
loadSkinToCanvas(skinCanvas, image)
|
||||
skinTexture = new THREE.CanvasTexture(skinCanvas)
|
||||
if (skinUrl === stevePng) {
|
||||
this.defaultSteveTexture = skinTexture
|
||||
|
||||
if (typeof skinUrl !== 'string') throw new Error('Invalid skin url')
|
||||
void this.loadAndApplySkin(entityId, skinUrl).then(() => {
|
||||
if (capeUrl) {
|
||||
if (capeUrl === true && username) {
|
||||
capeUrl = getLookupUrl(username, 'cape')
|
||||
}
|
||||
if (typeof capeUrl === 'string') {
|
||||
void this.loadAndApplyCape(entityId, capeUrl)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
playerObject.cape.visible = false
|
||||
if (!capeUrl) {
|
||||
playerObject.backEquipment = null
|
||||
playerObject.elytra.map = null
|
||||
if (playerObject.cape.map) {
|
||||
playerObject.cape.map.dispose()
|
||||
}
|
||||
playerObject.cape.map = null
|
||||
}
|
||||
}
|
||||
|
||||
private async loadAndApplySkin (entityId: string | number, skinUrl: string) {
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
try {
|
||||
let playerCustomSkinImage: HTMLImageElement | undefined
|
||||
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
let skinTexture: THREE.Texture
|
||||
if (skinUrl === stevePngUrl) {
|
||||
skinTexture = await steveTexture
|
||||
} else {
|
||||
const { canvas: skinCanvas, image } = await loadSkinImage(skinUrl)
|
||||
playerCustomSkinImage = image
|
||||
skinTexture = new THREE.CanvasTexture(skinCanvas)
|
||||
}
|
||||
|
||||
skinTexture.magFilter = THREE.NearestFilter
|
||||
skinTexture.minFilter = THREE.NearestFilter
|
||||
skinTexture.needsUpdate = true
|
||||
playerObject.skin.map = skinTexture
|
||||
playerObject.skin.map = skinTexture as any
|
||||
playerObject.skin.modelType = inferModelType(skinTexture.image)
|
||||
|
||||
const earsCanvas = document.createElement('canvas')
|
||||
loadEarsToCanvasFromSkin(earsCanvas, image)
|
||||
if (isCanvasBlank(earsCanvas)) {
|
||||
if (playerCustomSkinImage) {
|
||||
loadEarsToCanvasFromSkin(earsCanvas, playerCustomSkinImage)
|
||||
}
|
||||
if (this.isCanvasBlank(earsCanvas)) {
|
||||
playerObject.ears.map = null
|
||||
playerObject.ears.visible = false
|
||||
} else {
|
||||
|
|
@ -365,47 +413,38 @@ export class Entities extends EventEmitter {
|
|||
playerObject.ears.visible = true
|
||||
}
|
||||
this.onSkinUpdate?.()
|
||||
if (capeUrl) {
|
||||
if (capeUrl === true) capeUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=cape`
|
||||
loadImage(capeUrl).then(capeImage => {
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
const capeCanvas = document.createElement('canvas')
|
||||
loadCapeToCanvas(capeCanvas, capeImage)
|
||||
|
||||
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
||||
capeTexture.magFilter = THREE.NearestFilter
|
||||
capeTexture.minFilter = THREE.NearestFilter
|
||||
capeTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.cape.map = capeTexture
|
||||
playerObject.cape.visible = true
|
||||
//@ts-expect-error
|
||||
playerObject.elytra.map = capeTexture
|
||||
this.onSkinUpdate?.()
|
||||
|
||||
if (!playerObject.backEquipment) {
|
||||
playerObject.backEquipment = 'cape'
|
||||
}
|
||||
}, () => { })
|
||||
}
|
||||
}, () => { })
|
||||
|
||||
|
||||
playerObject.cape.visible = false
|
||||
if (!capeUrl) {
|
||||
playerObject.backEquipment = null
|
||||
playerObject.elytra.map = null
|
||||
if (playerObject.cape.map) {
|
||||
playerObject.cape.map.dispose()
|
||||
}
|
||||
playerObject.cape.map = null
|
||||
} catch (error) {
|
||||
console.error('Error loading skin:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function isCanvasBlank (canvas) {
|
||||
return !canvas.getContext('2d')
|
||||
.getImageData(0, 0, canvas.width, canvas.height).data
|
||||
.some(channel => channel !== 0)
|
||||
private async loadAndApplyCape (entityId: string | number, capeUrl: string) {
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
try {
|
||||
const { canvas: capeCanvas, image: capeImage } = await loadSkinImage(capeUrl)
|
||||
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
|
||||
loadCapeToCanvas(capeCanvas, capeImage)
|
||||
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
||||
capeTexture.magFilter = THREE.NearestFilter
|
||||
capeTexture.minFilter = THREE.NearestFilter
|
||||
capeTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.cape.map = capeTexture
|
||||
playerObject.cape.visible = true
|
||||
//@ts-expect-error
|
||||
playerObject.elytra.map = capeTexture
|
||||
this.onSkinUpdate?.()
|
||||
|
||||
if (!playerObject.backEquipment) {
|
||||
playerObject.backEquipment = 'cape'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading cape:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -447,11 +486,11 @@ export class Entities extends EventEmitter {
|
|||
return typeof component === 'string' ? component : component.text ?? ''
|
||||
}
|
||||
|
||||
getItemMesh (item, isDropped = false) {
|
||||
const textureUv = this.getItemUv?.(item)
|
||||
getItemMesh (item, specificProps: ItemSpecificContextProperties) {
|
||||
const textureUv = this.getItemUv?.(item, specificProps)
|
||||
if (textureUv && 'resolvedModel' in textureUv) {
|
||||
const mesh = getBlockMeshFromModel(this.viewer.world.material, textureUv.resolvedModel, textureUv.modelName)
|
||||
if (isDropped) {
|
||||
if (specificProps['minecraft:display_context'] === 'ground') {
|
||||
const SCALE = 0.5
|
||||
mesh.scale.set(SCALE, SCALE, SCALE)
|
||||
mesh.position.set(0, 0.2, 0)
|
||||
|
|
@ -547,7 +586,9 @@ export class Entities extends EventEmitter {
|
|||
if (entity.name === 'item') {
|
||||
const item = entity.metadata?.find((m: any) => typeof m === 'object' && m?.itemCount)
|
||||
if (item) {
|
||||
const object = this.getItemMesh(item, true)
|
||||
const object = this.getItemMesh(item, {
|
||||
'minecraft:display_context': 'ground',
|
||||
})
|
||||
if (object) {
|
||||
mesh = object.mesh
|
||||
mesh.scale.set(0.5, 0.5, 0.5)
|
||||
|
|
@ -627,12 +668,14 @@ export class Entities extends EventEmitter {
|
|||
this.viewer.scene.add(group)
|
||||
|
||||
e = group
|
||||
e.name = 'entity'
|
||||
e['realName'] = entity.name
|
||||
this.entities[entity.id] = e
|
||||
|
||||
this.emit('add', entity)
|
||||
|
||||
if (isPlayerModel) {
|
||||
this.updatePlayerSkin(entity.id, entity.username, entity.uuid, overrides?.texture || stevePng)
|
||||
this.updatePlayerSkin(entity.id, entity.username, entity.uuid, overrides?.texture || stevePngUrl)
|
||||
}
|
||||
this.setDebugMode(this.debugMode, group)
|
||||
this.setRendering(this.rendering, group)
|
||||
|
|
@ -781,7 +824,9 @@ export class Entities extends EventEmitter {
|
|||
mesh.scale.set(16 / 12, 16 / 12, 1)
|
||||
this.addMapModel(e, mapNumber, rotation)
|
||||
} else {
|
||||
const itemMesh = this.getItemMesh(item)
|
||||
const itemMesh = this.getItemMesh(item, {
|
||||
'minecraft:display_context': 'fixed',
|
||||
})
|
||||
if (itemMesh) {
|
||||
itemMesh.mesh.position.set(0, 0, 0.43)
|
||||
itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
|
||||
|
|
@ -945,7 +990,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
console.error('Error decoding player head texture:', err)
|
||||
}
|
||||
} else {
|
||||
texturePath = stevePng
|
||||
texturePath = stevePngUrl
|
||||
}
|
||||
}
|
||||
const armorMaterial = itemParts[0]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import * as THREE from 'three'
|
|||
import { OBJLoader } from 'three-stdlib'
|
||||
import huskPng from 'mc-assets/dist/other-textures/latest/entity/zombie/husk.png'
|
||||
import { Vec3 } from 'vec3'
|
||||
import spectralArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/spectral_arrow.png'
|
||||
import tippedArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
|
||||
import { WorldRendererCommon } from '../worldrendererCommon'
|
||||
import entities from './entities.json'
|
||||
import { externalModels } from './objModels'
|
||||
|
|
@ -457,6 +459,13 @@ export class EntityMesh {
|
|||
return
|
||||
}
|
||||
|
||||
// if (originalType === 'arrow') {
|
||||
// overrides.textures = {
|
||||
// 'default': arrowTexture,
|
||||
// ...overrides.textures,
|
||||
// }
|
||||
// }
|
||||
|
||||
const e = getEntity(type)
|
||||
if (!e) {
|
||||
if (knownNotHandled.includes(type)) return
|
||||
|
|
|
|||
|
|
@ -1,32 +1,21 @@
|
|||
import * as THREE from 'three'
|
||||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { getLookupUrl, loadSkinImage, steveTexture } from './utils/skins'
|
||||
|
||||
let steveTexture: THREE.Texture
|
||||
export const getMyHand = async (image?: string) => {
|
||||
export const getMyHand = async (image?: string, userName?: string) => {
|
||||
let newMap: THREE.Texture
|
||||
if (!image && steveTexture) {
|
||||
newMap = steveTexture
|
||||
if (!image && !userName) {
|
||||
newMap = await steveTexture
|
||||
} else {
|
||||
image ??= stevePng
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
const img = new Image()
|
||||
img.src = image
|
||||
await new Promise<void>(resolve => {
|
||||
img.onload = () => {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
loadSkinToCanvas(skinCanvas, img)
|
||||
newMap = new THREE.CanvasTexture(skinCanvas)
|
||||
// newMap.flipY = false
|
||||
newMap.magFilter = THREE.NearestFilter
|
||||
newMap.minFilter = THREE.NearestFilter
|
||||
if (!image) {
|
||||
steveTexture = newMap
|
||||
image = getLookupUrl(userName!, 'skin')
|
||||
}
|
||||
const { canvas } = await loadSkinImage(image)
|
||||
newMap = new THREE.CanvasTexture(canvas)
|
||||
}
|
||||
|
||||
newMap.magFilter = THREE.NearestFilter
|
||||
newMap.minFilter = THREE.NearestFilter
|
||||
// right arm
|
||||
const box = new THREE.BoxGeometry()
|
||||
const material = new THREE.MeshStandardMaterial()
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,8 @@ import { Vec3 } from 'vec3'
|
|||
import { World } from './world'
|
||||
import { getSectionGeometry, setBlockStatesData as setMesherData } from './models'
|
||||
|
||||
globalThis.structuredClone ??= (value) => JSON.parse(JSON.stringify(value))
|
||||
|
||||
if (module.require) {
|
||||
// If we are in a node environement, we need to fake some env variables
|
||||
const r = module.require
|
||||
|
|
|
|||
169
renderer/viewer/lib/smoothSwitcher.ts
Normal file
169
renderer/viewer/lib/smoothSwitcher.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { AnimationController } from './animationController'
|
||||
|
||||
export type StateProperties = Record<string, number>
|
||||
export type StateGetterFn = () => StateProperties
|
||||
export type StateSetterFn = (property: string, value: number) => void
|
||||
|
||||
// Speed in units per second for each property type
|
||||
const DEFAULT_SPEEDS = {
|
||||
x: 3000, // pixels/units per second
|
||||
y: 3000,
|
||||
z: 3000,
|
||||
rotation: Math.PI, // radians per second
|
||||
scale: 1, // scale units per second
|
||||
default: 3000 // default speed for unknown properties
|
||||
}
|
||||
|
||||
export class SmoothSwitcher {
|
||||
private readonly animationController = new AnimationController()
|
||||
// private readonly currentState: StateProperties = {}
|
||||
private readonly defaultState: StateProperties
|
||||
private readonly speeds: Record<string, number>
|
||||
public currentStateName = ''
|
||||
public transitioningToStateName = ''
|
||||
|
||||
constructor (
|
||||
public getState: StateGetterFn,
|
||||
public setState: StateSetterFn,
|
||||
speeds?: Partial<Record<string, number>>
|
||||
) {
|
||||
|
||||
// Initialize speeds with defaults and overrides
|
||||
this.speeds = { ...DEFAULT_SPEEDS }
|
||||
if (speeds) {
|
||||
Object.assign(this.speeds, speeds)
|
||||
}
|
||||
|
||||
// Store initial values
|
||||
this.defaultState = this.getState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate transition duration based on the largest property change
|
||||
*/
|
||||
private calculateDuration (newState: Partial<StateProperties>): number {
|
||||
let maxDuration = 0
|
||||
const currentState = this.getState()
|
||||
|
||||
for (const [key, targetValue] of Object.entries(newState)) {
|
||||
const currentValue = currentState[key]
|
||||
const diff = Math.abs(targetValue! - currentValue)
|
||||
const speed = this.getPropertySpeed(key)
|
||||
const duration = (diff / speed) * 1000 // Convert to milliseconds
|
||||
|
||||
maxDuration = Math.max(maxDuration, duration)
|
||||
}
|
||||
|
||||
// Ensure minimum duration of 50ms and maximum of 2000ms
|
||||
return Math.min(Math.max(maxDuration, 200), 2000)
|
||||
}
|
||||
|
||||
private getPropertySpeed (property: string): number {
|
||||
// Check for specific property speed
|
||||
if (property in this.speeds) {
|
||||
return this.speeds[property]
|
||||
}
|
||||
|
||||
// Check for property type (rotation, scale, etc.)
|
||||
if (property.toLowerCase().includes('rotation')) return this.speeds.rotation
|
||||
if (property.toLowerCase().includes('scale')) return this.speeds.scale
|
||||
if (property.toLowerCase() === 'x' || property.toLowerCase() === 'y' || property.toLowerCase() === 'z') {
|
||||
return this.speeds[property]
|
||||
}
|
||||
|
||||
return this.speeds.default
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a transition to a new state
|
||||
* @param newState Partial state - only need to specify properties that change
|
||||
* @param easing Easing function to use
|
||||
*/
|
||||
startTransition (
|
||||
newState: Partial<StateProperties>,
|
||||
stateName?: string,
|
||||
onEnd?: () => void,
|
||||
easing: (amount: number) => number = tweenJs.Easing.Linear.None,
|
||||
onCancelled?: () => void
|
||||
): void {
|
||||
if (this.isTransitioning) {
|
||||
onCancelled?.()
|
||||
this.animationController.forceFinish()
|
||||
}
|
||||
|
||||
this.transitioningToStateName = stateName ?? ''
|
||||
const state = this.getState()
|
||||
|
||||
const duration = this.calculateDuration(newState)
|
||||
// console.log('duration', duration, JSON.stringify(state), JSON.stringify(newState))
|
||||
|
||||
void this.animationController.startAnimation(() => {
|
||||
const group = new tweenJs.Group()
|
||||
new tweenJs.Tween(state, group)
|
||||
.to(newState, duration)
|
||||
.easing(easing)
|
||||
.onUpdate((obj) => {
|
||||
for (const key of Object.keys(obj)) {
|
||||
this.setState(key, obj[key])
|
||||
}
|
||||
})
|
||||
.onComplete(() => {
|
||||
this.animationController.forceFinish()
|
||||
this.currentStateName = this.transitioningToStateName
|
||||
this.transitioningToStateName = ''
|
||||
onEnd?.()
|
||||
})
|
||||
.start()
|
||||
return group
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default state
|
||||
*/
|
||||
reset (): void {
|
||||
this.startTransition(this.defaultState)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the animation (should be called in your render/update loop)
|
||||
*/
|
||||
update (): void {
|
||||
this.animationController.update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Force finish the current transition
|
||||
*/
|
||||
forceFinish (): void {
|
||||
this.animationController.forceFinish()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new transition to the specified state
|
||||
*/
|
||||
transitionTo (
|
||||
newState: Partial<StateProperties>,
|
||||
stateName?: string,
|
||||
onEnd?: () => void,
|
||||
onCancelled?: () => void
|
||||
): void {
|
||||
this.startTransition(newState, stateName, onEnd, tweenJs.Easing.Linear.None, onCancelled)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of a property
|
||||
*/
|
||||
getCurrentValue (property: string): number {
|
||||
return this.getState()[property]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently transitioning
|
||||
*/
|
||||
get isTransitioning (): boolean {
|
||||
return this.animationController.isActive
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
export const disposeObject = (obj: THREE.Object3D) => {
|
||||
export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => {
|
||||
// not cleaning texture there as it might be used by other objects, but would be good to also do that
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry?.dispose?.()
|
||||
|
|
@ -8,6 +8,11 @@ export const disposeObject = (obj: THREE.Object3D) => {
|
|||
}
|
||||
if (obj.children) {
|
||||
// eslint-disable-next-line unicorn/no-array-for-each
|
||||
obj.children.forEach(disposeObject)
|
||||
obj.children.forEach(child => disposeObject(child, cleanTextures))
|
||||
}
|
||||
if (cleanTextures) {
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.material?.map?.dispose?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
23
renderer/viewer/lib/utils/proxy.ts
Normal file
23
renderer/viewer/lib/utils/proxy.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
export function watchProperty<T extends Record<string, any>, K> (asyncGetter: (value: T[keyof T]) => Promise<K>, valtioProxy: T, key: keyof T, readySetter: (res: K) => void, cleanup?: (res: K) => void) {
|
||||
let i = 0
|
||||
let lastRes: K | undefined
|
||||
const request = async () => {
|
||||
const req = ++i
|
||||
const res = await asyncGetter(valtioProxy[key])
|
||||
if (req === i) {
|
||||
if (lastRes) {
|
||||
cleanup?.(lastRes)
|
||||
}
|
||||
readySetter(res)
|
||||
lastRes = res
|
||||
} else {
|
||||
// rejected
|
||||
cleanup?.(res)
|
||||
}
|
||||
}
|
||||
void request()
|
||||
return subscribeKey(valtioProxy, key, request)
|
||||
}
|
||||
27
renderer/viewer/lib/utils/skins.ts
Normal file
27
renderer/viewer/lib/utils/skins.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import * as THREE from 'three'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-export-from
|
||||
export const stevePngUrl = stevePng
|
||||
export const steveTexture = new THREE.TextureLoader().loadAsync(stevePng)
|
||||
|
||||
export async function loadImageFromUrl (imageUrl: string): Promise<HTMLImageElement> {
|
||||
const img = new Image()
|
||||
img.src = imageUrl
|
||||
await new Promise<void>(resolve => {
|
||||
img.onload = () => resolve()
|
||||
})
|
||||
return img
|
||||
}
|
||||
|
||||
export function getLookupUrl (username: string, type: 'skin' | 'cape'): string {
|
||||
return `https://mulv.tycrek.dev/api/lookup?username=${username}&type=${type}`
|
||||
}
|
||||
|
||||
export async function loadSkinImage (skinUrl: string): Promise<{ canvas: HTMLCanvasElement, image: HTMLImageElement }> {
|
||||
const image = await loadImageFromUrl(skinUrl)
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
loadSkinToCanvas(skinCanvas, image)
|
||||
return { canvas: skinCanvas, image }
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ import * as THREE from 'three'
|
|||
import { Vec3 } from 'vec3'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
|
@ -11,6 +10,8 @@ import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig }
|
|||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
import { addNewStat } from './ui/newStats'
|
||||
import { getMyHand } from './hand'
|
||||
import { IPlayerState, BasePlayerState } from './basePlayerState'
|
||||
import { CameraBobbing } from './cameraBobbing'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
|
|
@ -21,14 +22,12 @@ export class Viewer {
|
|||
// primitives: Primitives
|
||||
domElement: HTMLCanvasElement
|
||||
playerHeight = 1.62
|
||||
isSneaking = false
|
||||
threeJsWorld: WorldRendererThree
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
audioListener: THREE.AudioListener
|
||||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
|
||||
getMineflayerBot (): void | Record<string, any> {} // to be overridden
|
||||
private readonly cameraBobbing: CameraBobbing
|
||||
|
||||
get camera () {
|
||||
return this.world.camera
|
||||
|
|
@ -38,18 +37,19 @@ export class Viewer {
|
|||
this.world.camera = camera
|
||||
}
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
|
||||
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig, public playerState: IPlayerState = new BasePlayerState()) {
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig, this.playerState)
|
||||
this.setWorld()
|
||||
this.resetScene()
|
||||
this.entities = new Entities(this)
|
||||
// this.primitives = new Primitives(this.scene, this.camera)
|
||||
this.cameraBobbing = new CameraBobbing()
|
||||
|
||||
this.domElement = renderer.domElement
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ export class Viewer {
|
|||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const { mesh } = this.entities.getItemMesh({
|
||||
itemId: 541,
|
||||
})!
|
||||
}, {})!
|
||||
mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
||||
// mesh.scale.set(0.5, 0.5, 0.5)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
|
|
@ -161,12 +161,31 @@ export class Viewer {
|
|||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
let yOffset = this.getMineflayerBot()?.entity?.eyeHeight ?? this.playerHeight
|
||||
if (this.isSneaking) yOffset -= 0.3
|
||||
const yOffset = this.playerState.getEyeHeight()
|
||||
// if (this.playerState.isSneaking()) yOffset -= 0.3
|
||||
|
||||
this.world.camera = cam as THREE.PerspectiveCamera
|
||||
|
||||
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
|
||||
// // Update camera bobbing based on movement state
|
||||
// const velocity = this.playerState.getVelocity()
|
||||
// const movementState = this.playerState.getMovementState()
|
||||
// const isMoving = movementState === 'SPRINTING' || movementState === 'WALKING'
|
||||
// const speed = Math.hypot(velocity.x, velocity.z)
|
||||
|
||||
// // Update bobbing state
|
||||
// this.cameraBobbing.updateWalkDistance(speed)
|
||||
// this.cameraBobbing.updateBobAmount(isMoving)
|
||||
|
||||
// // Get bobbing offsets
|
||||
// const bobbing = isMoving ? this.cameraBobbing.getBobbing() : { position: { x: 0, y: 0 }, rotation: { x: 0, z: 0 } }
|
||||
|
||||
// // Apply camera position with bobbing
|
||||
// const finalPos = pos ? pos.offset(bobbing.position.x, yOffset + bobbing.position.y, 0) : null
|
||||
// this.world.updateCamera(finalPos, yaw + bobbing.rotation.x, pitch)
|
||||
|
||||
// // Apply roll rotation separately since updateCamera doesn't handle it
|
||||
// this.camera.rotation.z = bobbing.rotation.z
|
||||
}
|
||||
|
||||
playSound (position: Vec3, path: string, volume = 1, pitch = 1) {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class ViewerWrapper {
|
|||
if (this.globalObject.stopLoop) return
|
||||
for (const fn of beforeRenderFrame) fn()
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
if (this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return
|
||||
if (!viewer || this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return
|
||||
const renderInterval = (this.windowFocused ? this.renderInterval : this.renderIntervalUnfocused) ?? this.renderInterval
|
||||
if (renderInterval) {
|
||||
this.delta += time - this.lastTime
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Vec3 } from 'vec3'
|
|||
import { BotEvents } from 'mineflayer'
|
||||
import { getItemFromBlock } from '../../../src/chatUtils'
|
||||
import { delayedIterator } from '../../playground/shared'
|
||||
import { playerState } from '../../../src/mineflayer/playerState'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
export type ChunkPosKey = string
|
||||
|
|
@ -102,36 +103,8 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
},
|
||||
time: () => {
|
||||
this.emitter.emit('time', bot.time.timeOfDay)
|
||||
},
|
||||
heldItemChanged () {
|
||||
handChanged(false)
|
||||
},
|
||||
}
|
||||
} satisfies Partial<BotEvents>
|
||||
const handChanged = (isLeftHand: boolean) => {
|
||||
const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem
|
||||
if (!newItem) {
|
||||
viewer.world.onHandItemSwitch(undefined, isLeftHand)
|
||||
return
|
||||
}
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
// todo clean types
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
// todo item props
|
||||
viewer.world.onHandItemSwitch({
|
||||
name: newItem.name,
|
||||
properties: blockProperties,
|
||||
id: newItem.type,
|
||||
type: block ? 'block' : 'item',
|
||||
fullItem: newItem,
|
||||
}, isLeftHand)
|
||||
}
|
||||
bot.inventory.on('updateSlot', (index) => {
|
||||
if (index === 45) {
|
||||
handChanged(true)
|
||||
}
|
||||
})
|
||||
handChanged(false)
|
||||
handChanged(true)
|
||||
|
||||
|
||||
bot._client.on('update_light', ({ chunkX, chunkZ }) => {
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
|
|||
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
|
||||
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
|
||||
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
|
||||
import { AtlasParser } from 'mc-assets'
|
||||
import { AtlasParser, getLoadedItemDefinitionsStore } from 'mc-assets'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { LineMaterial } from 'three-stdlib'
|
||||
import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
|
||||
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
||||
import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json'
|
||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajorVersion } from '../../../src/utils'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
|
|
@ -34,7 +35,8 @@ export const defaultWorldRendererConfig = {
|
|||
numWorkers: 4,
|
||||
isPlayground: false,
|
||||
// game renderer setting actually
|
||||
displayHand: false
|
||||
showHand: false,
|
||||
viewBobbing: false
|
||||
}
|
||||
|
||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||
|
|
@ -119,13 +121,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
sourceData = {
|
||||
blocksAtlases,
|
||||
itemsAtlases
|
||||
itemsAtlases,
|
||||
itemDefinitionsJson
|
||||
}
|
||||
customTextures: {
|
||||
items?: CustomTexturesData
|
||||
blocks?: CustomTexturesData
|
||||
armor?: CustomTexturesData
|
||||
} = {}
|
||||
itemsDefinitionsStore = getLoadedItemDefinitionsStore(this.sourceData.itemDefinitionsJson)
|
||||
workersProcessAverageTime = 0
|
||||
workersProcessAverageTimeCount = 0
|
||||
maxWorkersProcessTime = 0
|
||||
|
|
@ -248,7 +252,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
}
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined, isLeftHand: boolean): void { }
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }
|
||||
|
||||
abstract handleWorkerMessage (data: WorkerReceive): void
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { disposeObject } from './threeJsUtils'
|
|||
import HoldingBlock, { HandItemBlock } from './holdingBlock'
|
||||
import { addNewStat } from './ui/newStats'
|
||||
import { MesherGeometryOutput } from './mesher/shared'
|
||||
import { IPlayerState } from './basePlayerState'
|
||||
import { getMesh } from './entity/EntityMesh'
|
||||
import { armorModel } from './entity/armorModels'
|
||||
|
||||
|
|
@ -36,48 +37,29 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).blocksCount, 0)
|
||||
}
|
||||
|
||||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
|
||||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig, public playerState: IPlayerState) {
|
||||
super(config)
|
||||
this.rendererDevice = `${WorldRendererThree.getRendererInfo(this.renderer)} powered by three.js r${THREE.REVISION}`
|
||||
this.starField = new StarField(scene)
|
||||
this.holdingBlock = new HoldingBlock()
|
||||
this.holdingBlockLeft = new HoldingBlock()
|
||||
this.holdingBlockLeft.rightSide = false
|
||||
this.holdingBlock = new HoldingBlock(playerState, this.config)
|
||||
this.holdingBlockLeft = new HoldingBlock(playerState, this.config, true)
|
||||
|
||||
this.renderUpdateEmitter.on('itemsTextureDownloaded', () => {
|
||||
if (this.holdingBlock.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem)
|
||||
this.holdingBlock.toBeRenderedItem = undefined
|
||||
}
|
||||
if (this.holdingBlockLeft.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem, true)
|
||||
this.holdingBlockLeft.toBeRenderedItem = undefined
|
||||
}
|
||||
this.holdingBlock.ready = true
|
||||
this.holdingBlock.updateItem()
|
||||
this.holdingBlockLeft.ready = true
|
||||
this.holdingBlockLeft.updateItem()
|
||||
})
|
||||
|
||||
this.addDebugOverlay()
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined, isLeft = false) {
|
||||
if (!isLeft) {
|
||||
item ??= {
|
||||
type: 'hand',
|
||||
}
|
||||
}
|
||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||
if (!this.currentTextureImage) {
|
||||
holdingBlock.toBeRenderedItem = item
|
||||
return
|
||||
}
|
||||
void holdingBlock.initHandObject(item)
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean, isLeft = false) {
|
||||
const holdingBlock = isLeft ? this.holdingBlockLeft : this.holdingBlock
|
||||
if (isAnimationPlaying) {
|
||||
holdingBlock.startSwing()
|
||||
} else {
|
||||
void holdingBlock.stopSwing()
|
||||
holdingBlock.stopSwing()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,7 +234,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
||||
const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
||||
this.renderer.render(this.scene, cam)
|
||||
if (this.config.displayHand) {
|
||||
if (this.config.showHand) {
|
||||
this.holdingBlock.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
this.holdingBlockLeft.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
globalThis.resolveDnsFallback = async (hostname: string) => {
|
||||
const response = await fetchServerStatus(hostname)
|
||||
return response?.raw.srv_record ?? undefined
|
||||
}
|
||||
|
||||
export const isServerValid = (ip: string) => {
|
||||
const isInLocalNetwork = ip.startsWith('192.168.') ||
|
||||
ip.startsWith('10.') ||
|
||||
|
|
@ -51,4 +56,9 @@ export type ServerResponse = {
|
|||
// todo display via hammer icon
|
||||
software?: string
|
||||
plugins?: Array<{ name, version }>
|
||||
// port?: number
|
||||
srv_record?: {
|
||||
host: string
|
||||
port: number
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,15 +112,20 @@ const commands: Array<{
|
|||
{
|
||||
command: ['/pos'],
|
||||
async invoke ([type]) {
|
||||
let pos: string | undefined
|
||||
let pos: { x: number, y: number, z: number } | undefined
|
||||
if (type === 'block') {
|
||||
pos = window.cursorBlockRel()?.position?.toString().slice(1, -1)
|
||||
const blockPos = window.cursorBlockRel()?.position
|
||||
if (blockPos) {
|
||||
pos = { x: blockPos.x, y: blockPos.y, z: blockPos.z }
|
||||
}
|
||||
} else {
|
||||
pos = bot.entity.position.toString().slice(1, -1)
|
||||
const playerPos = bot.entity.position
|
||||
pos = { x: playerPos.x, y: playerPos.y, z: playerPos.z }
|
||||
}
|
||||
if (!pos) return
|
||||
await navigator.clipboard.writeText(pos)
|
||||
writeText(`Copied position to clipboard: ${pos}`)
|
||||
const formatted = `${pos.x.toFixed(2)} ${pos.y.toFixed(2)} ${pos.z.toFixed(2)}`
|
||||
await navigator.clipboard.writeText(formatted)
|
||||
writeText(`Copied position to clipboard: ${formatted}`)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ Object.defineProperty(window, 'debugSceneChunks', {
|
|||
},
|
||||
})
|
||||
|
||||
window.chunkKey = (xRel = 0, zRel = 0) => {
|
||||
const pos = bot.entity.position
|
||||
return `${(Math.floor(pos.x / 16) + xRel) * 16},${(Math.floor(pos.z / 16) + zRel) * 16}`
|
||||
}
|
||||
|
||||
window.sectionKey = (xRel = 0, yRel = 0, zRel = 0) => {
|
||||
const pos = bot.entity.position
|
||||
return `${(Math.floor(pos.x / 16) + xRel) * 16},${(Math.floor(pos.y / 16) + yRel) * 16},${(Math.floor(pos.z / 16) + zRel) * 16}`
|
||||
}
|
||||
|
||||
window.keys = (obj) => Object.keys(obj)
|
||||
window.values = (obj) => Object.values(obj)
|
||||
|
||||
window.len = (obj) => Object.keys(obj).length
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,16 @@
|
|||
import { proxy, ref, subscribe } from 'valtio'
|
||||
import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
|
||||
import type { OptionsGroupType } from './optionsGuiScheme'
|
||||
import { appQueryParams } from './appParams'
|
||||
|
||||
// todo: refactor structure with support of hideNext=false
|
||||
|
||||
const notHideableModalsWithoutForce = new Set(['app-status'])
|
||||
|
||||
if (appQueryParams.lockConnect) {
|
||||
notHideableModalsWithoutForce.add('editServer')
|
||||
}
|
||||
|
||||
type Modal = ({ elem?: HTMLElement & Record<string, any> } & { reactType: string })
|
||||
|
||||
type ContextMenuItem = { callback; label }
|
||||
|
|
|
|||
32
src/index.ts
32
src/index.ts
|
|
@ -66,6 +66,7 @@ import { isCypress } from './standaloneUtils'
|
|||
import {
|
||||
removePanorama
|
||||
} from './panorama'
|
||||
import { getItemDefinition } from 'mc-assets/dist/itemDefinitions'
|
||||
|
||||
import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
|
||||
import defaultServerOptions from './defaultLocalServerOptions'
|
||||
|
|
@ -104,6 +105,7 @@ import { getWebsocketStream } from './mineflayer/websocket-core'
|
|||
import { appQueryParams, appQueryParamsArray } from './appParams'
|
||||
import { updateCursor } from './cameraRotationControls'
|
||||
import { pingServerVersion } from './mineflayer/minecraft-protocol-extra'
|
||||
import { playerState, PlayerStateManager } from './mineflayer/playerState'
|
||||
import { states } from 'minecraft-protocol'
|
||||
import { initMotionTracking } from './react/uiMotion'
|
||||
import { UserError } from './mineflayer/userError'
|
||||
|
|
@ -161,21 +163,29 @@ if (isIphone) {
|
|||
if (appQueryParams.testCrashApp === '2') throw new Error('test')
|
||||
|
||||
// Create viewer
|
||||
const viewer: import('renderer/viewer/lib/viewer').Viewer = new Viewer(renderer)
|
||||
const viewer: import('renderer/viewer/lib/viewer').Viewer = new Viewer(renderer, undefined, playerState)
|
||||
window.viewer = viewer
|
||||
viewer.getMineflayerBot = () => bot
|
||||
// todo unify
|
||||
viewer.entities.getItemUv = (item) => {
|
||||
viewer.entities.getItemUv = (item, specificProps) => {
|
||||
const idOrName = item.itemId ?? item.blockId
|
||||
try {
|
||||
const name = typeof idOrName === 'number' ? loadedData.items[idOrName]?.name : idOrName
|
||||
if (!name) throw new Error(`Item not found: ${idOrName}`)
|
||||
|
||||
const renderInfo = renderSlot({
|
||||
name,
|
||||
nbt: null,
|
||||
...item
|
||||
const itemSelector = playerState.getItemSelector({
|
||||
...specificProps
|
||||
})
|
||||
const model = getItemDefinition(viewer.world.itemsDefinitionsStore, {
|
||||
name,
|
||||
version: viewer.world.texturesVersion!,
|
||||
properties: itemSelector
|
||||
})?.model ?? name
|
||||
|
||||
const renderInfo = renderSlot({
|
||||
...item,
|
||||
nbt: null,
|
||||
name: model,
|
||||
}, false, true)
|
||||
|
||||
if (!renderInfo) throw new Error(`Failed to get render info for item ${name}`)
|
||||
|
||||
|
|
@ -409,10 +419,12 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
throw err
|
||||
}
|
||||
}
|
||||
const oldStatus = appStatusState.status
|
||||
setLoadingScreenStatus('Loading minecraft assets')
|
||||
viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json')
|
||||
void viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
|
||||
miscUiState.loadedDataVersion = version
|
||||
setLoadingScreenStatus(oldStatus)
|
||||
}
|
||||
|
||||
let finalVersion = connectOptions.botVersion || (singleplayer ? serverOptions.version : undefined)
|
||||
|
|
@ -744,6 +756,8 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
customEvents.emit('gameLoaded')
|
||||
if (p2pConnectTimeout) clearTimeout(p2pConnectTimeout)
|
||||
|
||||
playerState.onlineMode = !!connectOptions.authenticatedAccount
|
||||
|
||||
setLoadingScreenStatus('Placing blocks (starting viewer)')
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
connectOptions.onSuccessfulPlay?.()
|
||||
|
|
@ -997,6 +1011,4 @@ if (initialLoader) {
|
|||
}
|
||||
window.pageLoaded = true
|
||||
|
||||
if (!reconnectOptions) {
|
||||
void possiblyHandleStateVariable()
|
||||
}
|
||||
void possiblyHandleStateVariable()
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ const getImage = ({ path = undefined as string | undefined, texture = undefined
|
|||
return loadedImagesCache.get(loadPath)
|
||||
}
|
||||
|
||||
export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false): {
|
||||
export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false, fullBlockModelSupport = false): {
|
||||
texture: string,
|
||||
blockData?: Record<string, { slice, path }> & { resolvedModel: BlockModel },
|
||||
scale?: number,
|
||||
|
|
@ -198,7 +198,7 @@ export const renderSlot = (slot: GeneralInputItem, debugIsQuickbar = false): {
|
|||
let itemTexture
|
||||
try {
|
||||
assertDefined(viewer.world.itemsRenderer)
|
||||
itemTexture = viewer.world.itemsRenderer.getItemTexture(itemModelName) ?? viewer.world.itemsRenderer.getItemTexture('item/missing_texture')!
|
||||
itemTexture = viewer.world.itemsRenderer.getItemTexture(itemModelName, {}, false, fullBlockModelSupport) ?? viewer.world.itemsRenderer.getItemTexture('item/missing_texture')!
|
||||
} catch (err) {
|
||||
inGameError(`Failed to render item ${itemModelName} (original: ${originalItemName}) on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.stack}`)
|
||||
itemTexture = viewer.world.itemsRenderer!.getItemTexture('block/errored')!
|
||||
|
|
|
|||
200
src/mineflayer/playerState.ts
Normal file
200
src/mineflayer/playerState.ts
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { IPlayerState, ItemSpecificContextProperties, MovementState, PlayerStateEvents } from 'renderer/viewer/lib/basePlayerState'
|
||||
import { HandItemBlock } from 'renderer/viewer/lib/holdingBlock'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { ItemSelector } from 'mc-assets/dist/itemDefinitions'
|
||||
import { proxy } from 'valtio'
|
||||
import { gameAdditionalState } from '../globalState'
|
||||
|
||||
export class PlayerStateManager implements IPlayerState {
|
||||
disableStateUpdates = false
|
||||
private static instance: PlayerStateManager
|
||||
readonly events = new EventEmitter() as TypedEmitter<PlayerStateEvents>
|
||||
|
||||
// Movement and physics state
|
||||
private lastVelocity = new Vec3(0, 0, 0)
|
||||
private movementState: MovementState = 'NOT_MOVING'
|
||||
private timeOffGround = 0
|
||||
private lastUpdateTime = performance.now()
|
||||
|
||||
// Held item state
|
||||
private heldItem?: HandItemBlock
|
||||
private offHandItem?: HandItemBlock
|
||||
private itemUsageTicks = 0
|
||||
private isUsingItem = false
|
||||
private ready = false
|
||||
onlineMode = false
|
||||
get username () {
|
||||
return bot.player?.username ?? ''
|
||||
}
|
||||
|
||||
reactive = proxy({
|
||||
playerSkin: undefined as string | undefined,
|
||||
})
|
||||
|
||||
static getInstance (): PlayerStateManager {
|
||||
if (!this.instance) {
|
||||
this.instance = new PlayerStateManager()
|
||||
}
|
||||
return this.instance
|
||||
}
|
||||
|
||||
private constructor () {
|
||||
this.updateState = this.updateState.bind(this)
|
||||
customEvents.on('mineflayerBotCreated', () => {
|
||||
this.ready = false
|
||||
bot.on('inject_allowed', () => {
|
||||
if (this.ready) return
|
||||
this.ready = true
|
||||
this.botCreated()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
private botCreated () {
|
||||
// Movement tracking
|
||||
bot.on('move', this.updateState)
|
||||
|
||||
// Item tracking
|
||||
bot.on('heldItemChanged', () => {
|
||||
return this.updateHeldItem(false)
|
||||
})
|
||||
bot.inventory.on('updateSlot', (index) => {
|
||||
if (index === 45) this.updateHeldItem(true)
|
||||
})
|
||||
bot.on('physicsTick', () => {
|
||||
if (this.isUsingItem) this.itemUsageTicks++
|
||||
})
|
||||
|
||||
// Initial held items setup
|
||||
this.updateHeldItem(false)
|
||||
this.updateHeldItem(true)
|
||||
}
|
||||
|
||||
// #region Movement and Physics State
|
||||
private updateState () {
|
||||
if (!bot.player?.entity || this.disableStateUpdates) return
|
||||
|
||||
const { velocity } = bot.player.entity
|
||||
const isOnGround = bot.entity.onGround
|
||||
const VELOCITY_THRESHOLD = 0.01
|
||||
const SPRINTING_VELOCITY = 0.15
|
||||
const OFF_GROUND_THRESHOLD = 0 // ms before switching to SNEAKING when off ground
|
||||
|
||||
const now = performance.now()
|
||||
const deltaTime = now - this.lastUpdateTime
|
||||
this.lastUpdateTime = now
|
||||
|
||||
this.lastVelocity = velocity
|
||||
|
||||
// Update time off ground
|
||||
if (isOnGround) {
|
||||
this.timeOffGround = 0
|
||||
} else {
|
||||
this.timeOffGround += deltaTime
|
||||
}
|
||||
|
||||
if (this.isSneaking() || this.isFlying() || (this.timeOffGround > OFF_GROUND_THRESHOLD)) {
|
||||
this.movementState = 'SNEAKING'
|
||||
} else if (Math.abs(velocity.x) > VELOCITY_THRESHOLD || Math.abs(velocity.z) > VELOCITY_THRESHOLD) {
|
||||
this.movementState = Math.abs(velocity.x) > SPRINTING_VELOCITY || Math.abs(velocity.z) > SPRINTING_VELOCITY
|
||||
? 'SPRINTING'
|
||||
: 'WALKING'
|
||||
} else {
|
||||
this.movementState = 'NOT_MOVING'
|
||||
}
|
||||
}
|
||||
|
||||
getMovementState (): MovementState {
|
||||
return this.movementState
|
||||
}
|
||||
|
||||
getVelocity (): Vec3 {
|
||||
return this.lastVelocity
|
||||
}
|
||||
|
||||
getEyeHeight (): number {
|
||||
return bot.controlState.sneak ? 1.27 : bot.entity?.['eyeHeight'] ?? 1.62
|
||||
}
|
||||
|
||||
isOnGround (): boolean {
|
||||
return bot?.entity?.onGround ?? true
|
||||
}
|
||||
|
||||
isSneaking (): boolean {
|
||||
return gameAdditionalState.isSneaking
|
||||
}
|
||||
|
||||
isFlying (): boolean {
|
||||
return gameAdditionalState.isFlying
|
||||
}
|
||||
|
||||
isSprinting (): boolean {
|
||||
return gameAdditionalState.isSprinting
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Held Item State
|
||||
private updateHeldItem (isLeftHand: boolean) {
|
||||
const newItem = isLeftHand ? bot.inventory.slots[45] : bot.heldItem
|
||||
if (!newItem) {
|
||||
if (isLeftHand) {
|
||||
this.offHandItem = undefined
|
||||
} else {
|
||||
this.heldItem = undefined
|
||||
}
|
||||
this.events.emit('heldItemChanged', undefined, isLeftHand)
|
||||
return
|
||||
}
|
||||
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
const item: HandItemBlock = {
|
||||
name: newItem.name,
|
||||
properties: blockProperties,
|
||||
id: newItem.type,
|
||||
type: block ? 'block' : 'item',
|
||||
fullItem: newItem,
|
||||
}
|
||||
|
||||
if (isLeftHand) {
|
||||
this.offHandItem = item
|
||||
} else {
|
||||
this.heldItem = item
|
||||
}
|
||||
this.events.emit('heldItemChanged', item, isLeftHand)
|
||||
}
|
||||
|
||||
startUsingItem () {
|
||||
if (this.isUsingItem) return
|
||||
this.isUsingItem = true
|
||||
this.itemUsageTicks = 0
|
||||
}
|
||||
|
||||
stopUsingItem () {
|
||||
this.isUsingItem = false
|
||||
this.itemUsageTicks = 0
|
||||
}
|
||||
|
||||
getItemUsageTicks (): number {
|
||||
return this.itemUsageTicks
|
||||
}
|
||||
|
||||
getHeldItem (isLeftHand = false): HandItemBlock | undefined {
|
||||
return isLeftHand ? this.offHandItem : this.heldItem
|
||||
}
|
||||
|
||||
getItemSelector (specificProperties: ItemSpecificContextProperties, item?: import('prismarine-item').Item): ItemSelector['properties'] {
|
||||
return {
|
||||
...specificProperties,
|
||||
'minecraft:date': new Date(),
|
||||
// "minecraft:context_dimension": bot.entityp,
|
||||
'minecraft:time': bot.time.timeOfDay / 24_000,
|
||||
}
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
export const playerState = PlayerStateManager.getInstance()
|
||||
window.playerState = playerState
|
||||
|
|
@ -91,7 +91,6 @@ export const guiOptionsScheme: {
|
|||
unit: '',
|
||||
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
|
||||
},
|
||||
handDisplay: {},
|
||||
renderDebug: {
|
||||
values: [
|
||||
'advanced',
|
||||
|
|
@ -248,6 +247,12 @@ export const guiOptionsScheme: {
|
|||
['classic', 'Classic']
|
||||
],
|
||||
},
|
||||
showHand: {
|
||||
text: 'Show Hand',
|
||||
},
|
||||
viewBobbing: {
|
||||
text: 'View Bobbing',
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ const defaultOptions = {
|
|||
enabledResourcepack: null as string | null,
|
||||
useVersionsTextures: 'latest',
|
||||
serverResourcePacks: 'prompt' as 'prompt' | 'always' | 'never',
|
||||
handDisplay: false,
|
||||
showHand: true,
|
||||
viewBobbing: true,
|
||||
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
|
||||
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export const parseServerAddress = (address: string | undefined, removeHttp = tru
|
|||
return { host: '', isWebSocket: false, serverIpFull: '' }
|
||||
}
|
||||
|
||||
if (/^ws:[^/]/.test(address)) address = address.replace('ws:', 'ws://')
|
||||
if (/^wss:[^/]/.test(address)) address = address.replace('wss:', 'wss://')
|
||||
const isWebSocket = address.startsWith('ws://') || address.startsWith('wss://')
|
||||
if (isWebSocket) {
|
||||
return { host: address, isWebSocket: true, serverIpFull: address }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import Screen from './Screen'
|
|||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import SelectGameVersion from './SelectGameVersion'
|
||||
import { useIsSmallWidth, usePassesWindowDimensions } from './simpleHooks'
|
||||
import { usePassesScaledDimensions } from './UIProvider'
|
||||
|
||||
export interface BaseServerInfo {
|
||||
ip: string
|
||||
|
|
@ -35,7 +35,7 @@ interface Props {
|
|||
const ELEMENTS_WIDTH = 190
|
||||
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
|
||||
const isSmallHeight = !usePassesWindowDimensions(null, 350)
|
||||
const isSmallHeight = !usePassesScaledDimensions(null, 350)
|
||||
const qsParamName = parseQs ? appQueryParams.name : undefined
|
||||
const qsParamIp = parseQs ? appQueryParams.ip : undefined
|
||||
const qsParamVersion = parseQs ? appQueryParams.version : undefined
|
||||
|
|
@ -54,7 +54,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
const [usernameOverride, setUsernameOverride] = React.useState(initialData?.usernameOverride ?? qsParamUsername ?? '')
|
||||
const lockConnect = qsParamLockConnect === 'true'
|
||||
|
||||
const smallWidth = useIsSmallWidth()
|
||||
const smallWidth = !usePassesScaledDimensions(400)
|
||||
const initialAccount = initialData?.authenticatedAccountOverride
|
||||
const [accountIndex, setAccountIndex] = React.useState(initialAccount === true ? -2 : initialAccount ? (accounts?.includes(initialAccount) ? accounts.indexOf(initialAccount) : -2) : -1)
|
||||
|
||||
|
|
@ -126,6 +126,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
}
|
||||
}, [])
|
||||
|
||||
const displayConnectButton = qsParamIp
|
||||
|
||||
return <Screen title={qsParamIp ? 'Connect to Server' : title} backdrop>
|
||||
<form
|
||||
style={{
|
||||
|
|
@ -139,9 +141,13 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
display: smallWidth ? 'flex' : 'grid',
|
||||
gap: 3,
|
||||
gridTemplateColumns: smallWidth ? '1fr' : '1fr 1fr'
|
||||
...(smallWidth ? {
|
||||
flexDirection: 'column',
|
||||
} : {
|
||||
gridTemplateColumns: '1fr 1fr'
|
||||
})
|
||||
}}
|
||||
>
|
||||
{!lockConnect && <>
|
||||
|
|
@ -219,17 +225,29 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
{!lockConnect && <>
|
||||
<ButtonWrapper onClick={() => {
|
||||
onBack()
|
||||
}}>Cancel</ButtonWrapper>
|
||||
<ButtonWrapper type='submit'>Save</ButtonWrapper>
|
||||
}}>
|
||||
Cancel
|
||||
</ButtonWrapper>
|
||||
<ButtonWrapper type='submit'>
|
||||
{displayConnectButton ? 'Save' : <strong>Save</strong>}
|
||||
</ButtonWrapper>
|
||||
</>}
|
||||
{qsParamIp && <div style={{ gridColumn: smallWidth ? '' : 'span 2', display: 'flex', justifyContent: 'center' }}>
|
||||
<ButtonWrapper
|
||||
data-test-id='connect-qs'
|
||||
onClick={() => {
|
||||
onQsConnect?.(commonUseOptions)
|
||||
}}
|
||||
><strong>Connect</strong></ButtonWrapper>
|
||||
</div>}
|
||||
{displayConnectButton && (
|
||||
<div style={{
|
||||
gridColumn: smallWidth ? '' : 'span 2',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<ButtonWrapper
|
||||
data-test-id='connect-qs'
|
||||
onClick={() => {
|
||||
onQsConnect?.(commonUseOptions)
|
||||
}}
|
||||
>
|
||||
<strong>Connect</strong>
|
||||
</ButtonWrapper>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Screen>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default () => {
|
|||
const [blockL, setBlockL] = useState(0)
|
||||
const [biomeId, setBiomeId] = useState(0)
|
||||
const [day, setDay] = useState(0)
|
||||
const [entitiesCount, setEntitiesCount] = useState(0)
|
||||
const [entitiesCount, setEntitiesCount] = useState('0')
|
||||
const [dimension, setDimension] = useState('')
|
||||
const [cursorBlock, setCursorBlock] = useState<Block | null>(null)
|
||||
const minecraftYaw = useRef(0)
|
||||
|
|
@ -106,7 +106,7 @@ export default () => {
|
|||
setDimension(bot.game.dimension)
|
||||
setDay(bot.time.day)
|
||||
setCursorBlock(bot.blockAtCursor(5))
|
||||
setEntitiesCount(Object.values(bot.entities).length)
|
||||
setEntitiesCount(`${viewer.entities.entitiesRenderingCount} (${Object.values(bot.entities).length})`)
|
||||
}, 100)
|
||||
|
||||
// @ts-expect-error
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function InnerSearch () {
|
|||
margin: 'auto',
|
||||
zIndex: 11,
|
||||
width: 'min-content',
|
||||
transform: 'scale(1.5)'
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -8,17 +8,24 @@ const componentActive = proxy({
|
|||
enabled: false
|
||||
})
|
||||
|
||||
subscribe(activeModalStack, () => {
|
||||
const checkModalAvailability = () => {
|
||||
const last = activeModalStack.at(-1)
|
||||
let withWildCardModal = false
|
||||
for (const modal of watchedModalsFromHooks) {
|
||||
for (const modal of watchedModalsFromHooks.value) {
|
||||
if (modal.endsWith('*') && last?.reactType.startsWith(modal.slice(0, -1))) {
|
||||
withWildCardModal = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
componentActive.enabled = !!last && !hardcodedKnownModals.some(x => last.reactType.startsWith(x)) && !watchedModalsFromHooks.has(last.reactType) && !withWildCardModal
|
||||
componentActive.enabled = !!last && !hardcodedKnownModals.some(x => last.reactType.startsWith(x)) && !watchedModalsFromHooks.value.has(last.reactType) && !withWildCardModal
|
||||
}
|
||||
|
||||
subscribe(activeModalStack, () => {
|
||||
checkModalAvailability()
|
||||
})
|
||||
subscribe(watchedModalsFromHooks, () => {
|
||||
checkModalAvailability()
|
||||
})
|
||||
|
||||
export default () => {
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import SignEditor, { ResultType } from './SignEditor'
|
|||
|
||||
|
||||
const isWysiwyg = async () => {
|
||||
const items = await bot.tabComplete('/', true, true)
|
||||
const commands = new Set<string>(['data'])
|
||||
const items = await bot.tabComplete('/data ', true, true)
|
||||
const commands = new Set<string>(['merge'])
|
||||
for (const item of items) {
|
||||
if (commands.has(item.match as unknown as string)) {
|
||||
if (commands.has((item.match ?? item) as unknown as string)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
src/react/UIProvider.tsx
Normal file
30
src/react/UIProvider.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useMedia } from 'react-use'
|
||||
|
||||
export const ScaleContext = createContext<number>(1)
|
||||
|
||||
export const useScale = () => useContext(ScaleContext)
|
||||
|
||||
export const UIProvider = ({ children, scale = 1 }) => {
|
||||
return (
|
||||
<ScaleContext.Provider value={scale}>
|
||||
{children}
|
||||
</ScaleContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const usePassesScaledDimensions = (minWidth: number | null = null, minHeight: number | null = null) => {
|
||||
const scale = useScale()
|
||||
const conditions: string[] = []
|
||||
|
||||
if (minWidth !== null) {
|
||||
conditions.push(`(min-width: ${minWidth * scale}px)`)
|
||||
}
|
||||
if (minHeight !== null) {
|
||||
conditions.push(`(min-height: ${minHeight * scale}px)`)
|
||||
}
|
||||
|
||||
const media = conditions.join(' and ') || 'all'
|
||||
return useMedia(media)
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { useMedia } from 'react-use'
|
||||
import { activeModalStack, miscUiState } from '../globalState'
|
||||
|
||||
export const watchedModalsFromHooks = new Set<string>()
|
||||
export const watchedModalsFromHooks = proxy({
|
||||
value: new Set<string>()
|
||||
})
|
||||
// todo should not be there
|
||||
export const hardcodedKnownModals = [
|
||||
'player_win:',
|
||||
|
|
@ -15,12 +17,12 @@ export const useUsingTouch = () => {
|
|||
}
|
||||
export const useIsModalActive = (modal: string, useIncludes = false) => {
|
||||
useMemo(() => {
|
||||
watchedModalsFromHooks.add(modal)
|
||||
watchedModalsFromHooks.value.add(modal)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
// watchedModalsFromHooks.add(modal)
|
||||
return () => {
|
||||
watchedModalsFromHooks.delete(modal)
|
||||
watchedModalsFromHooks.value.delete(modal)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ import DebugEdges from './react/DebugEdges'
|
|||
import GameInteractionOverlay from './react/GameInteractionOverlay'
|
||||
import MineflayerPluginHud from './react/MineflayerPluginHud'
|
||||
import MineflayerPluginConsole from './react/MineflayerPluginConsole'
|
||||
import { UIProvider } from './react/UIProvider'
|
||||
import { useAppScale } from './scaleInterface'
|
||||
|
||||
const RobustPortal = ({ children, to }) => {
|
||||
return createPortal(<PerComponentErrorBoundary>{children}</PerComponentErrorBoundary>, to)
|
||||
|
|
@ -175,45 +177,47 @@ const WidgetDisplay = ({ name, Component }) => {
|
|||
}
|
||||
|
||||
const App = () => {
|
||||
return <div>
|
||||
<ButtonAppProvider>
|
||||
<RobustPortal to={document.body}>
|
||||
<div className='overlay-bottom-scaled'>
|
||||
<InGameComponent>
|
||||
<HeldMapUi />
|
||||
</InGameComponent>
|
||||
</div>
|
||||
<div />
|
||||
</RobustPortal>
|
||||
<EnterFullscreenButton />
|
||||
<InGameUi />
|
||||
<RobustPortal to={document.querySelector('#ui-root')}>
|
||||
<AllWidgets />
|
||||
<SingleplayerProvider />
|
||||
<CreateWorldProvider />
|
||||
<AppStatusProvider />
|
||||
<KeybindingsScreenProvider />
|
||||
<SelectOption />
|
||||
<ServersListProvider />
|
||||
<OptionsRenderApp />
|
||||
<MainMenuRenderApp />
|
||||
<NotificationProvider />
|
||||
<TouchAreasControlsProvider />
|
||||
<SignInMessageProvider />
|
||||
<NoModalFoundProvider />
|
||||
{/* <GameHud>
|
||||
</GameHud> */}
|
||||
</RobustPortal>
|
||||
<RobustPortal to={document.body}>
|
||||
{/* todo correct mounting! */}
|
||||
<div className='overlay-top-scaled'>
|
||||
<GamepadUiCursor />
|
||||
</div>
|
||||
<div />
|
||||
<DebugEdges />
|
||||
</RobustPortal>
|
||||
</ButtonAppProvider>
|
||||
</div>
|
||||
const scale = useAppScale()
|
||||
return (
|
||||
<UIProvider scale={scale}>
|
||||
<div>
|
||||
<ButtonAppProvider>
|
||||
<RobustPortal to={document.body}>
|
||||
<div className='overlay-bottom-scaled'>
|
||||
<InGameComponent>
|
||||
<HeldMapUi />
|
||||
</InGameComponent>
|
||||
</div>
|
||||
<div />
|
||||
</RobustPortal>
|
||||
<EnterFullscreenButton />
|
||||
<InGameUi />
|
||||
<RobustPortal to={document.querySelector('#ui-root')}>
|
||||
<AllWidgets />
|
||||
<SingleplayerProvider />
|
||||
<CreateWorldProvider />
|
||||
<AppStatusProvider />
|
||||
<KeybindingsScreenProvider />
|
||||
<SelectOption />
|
||||
<ServersListProvider />
|
||||
<OptionsRenderApp />
|
||||
<MainMenuRenderApp />
|
||||
<NotificationProvider />
|
||||
<TouchAreasControlsProvider />
|
||||
<SignInMessageProvider />
|
||||
<NoModalFoundProvider />
|
||||
</RobustPortal>
|
||||
<RobustPortal to={document.body}>
|
||||
<div className='overlay-top-scaled'>
|
||||
<GamepadUiCursor />
|
||||
</div>
|
||||
<div />
|
||||
<DebugEdges />
|
||||
</RobustPortal>
|
||||
</ButtonAppProvider>
|
||||
</div>
|
||||
</UIProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const PerComponentErrorBoundary = ({ children }) => {
|
||||
|
|
|
|||
|
|
@ -1,67 +1,96 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { gameAdditionalState } from './globalState'
|
||||
import { options } from './optionsStorage'
|
||||
import { playerState } from './mineflayer/playerState'
|
||||
|
||||
let currentFov = 0
|
||||
let targetFov = 0
|
||||
let lastUpdateTime = 0
|
||||
const FOV_TRANSITION_DURATION = 200 // milliseconds
|
||||
|
||||
// TODO: These should be configured based on your game's settings
|
||||
const BASE_MOVEMENT_SPEED = 0.1 // Default walking speed in Minecraft
|
||||
const FOV_EFFECT_SCALE = 1 // Equivalent to Minecraft's FOV Effects slider
|
||||
|
||||
const updateFovAnimation = () => {
|
||||
if (currentFov === targetFov) return
|
||||
if (!bot) return
|
||||
|
||||
const now = performance.now()
|
||||
const elapsed = now - lastUpdateTime
|
||||
const progress = Math.min(elapsed / FOV_TRANSITION_DURATION, 1)
|
||||
// Calculate base FOV modifier
|
||||
let fovModifier = 1
|
||||
|
||||
// Smooth easing function
|
||||
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
|
||||
|
||||
currentFov += (targetFov - currentFov) * easeOutCubic(progress)
|
||||
|
||||
if (Math.abs(currentFov - targetFov) < 0.01) {
|
||||
currentFov = targetFov
|
||||
// Flying modifier
|
||||
if (gameAdditionalState.isFlying) {
|
||||
fovModifier *= 1.05
|
||||
}
|
||||
|
||||
viewer.camera.fov = currentFov
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
// Movement speed modifier
|
||||
// TODO: Get actual movement speed attribute value
|
||||
const movementSpeedAttr = (bot.entity?.attributes?.['generic.movement_speed'] || bot.entity?.attributes?.['minecraft:movement_speed'] || bot.entity?.attributes?.['movement_speed'] || bot.entity?.attributes?.['minecraft:movementSpeed'])?.value || BASE_MOVEMENT_SPEED
|
||||
let currentSpeed = BASE_MOVEMENT_SPEED
|
||||
// todo
|
||||
if (bot.controlState?.sprint && !bot.controlState?.sneak) {
|
||||
currentSpeed *= 1.3
|
||||
}
|
||||
fovModifier *= (currentSpeed / movementSpeedAttr + 1) / 2
|
||||
|
||||
// Validate fov modifier
|
||||
if (Math.abs(BASE_MOVEMENT_SPEED) < Number.EPSILON || isNaN(fovModifier) || !isFinite(fovModifier)) {
|
||||
fovModifier = 1
|
||||
}
|
||||
|
||||
// Item usage modifier
|
||||
if (playerState.getHeldItem()) {
|
||||
const heldItem = playerState.getHeldItem()
|
||||
if (heldItem?.name === 'bow' && playerState.getItemUsageTicks() > 0) {
|
||||
const ticksUsingItem = playerState.getItemUsageTicks()
|
||||
let usageProgress = ticksUsingItem / 20
|
||||
if (usageProgress > 1) {
|
||||
usageProgress = 1
|
||||
} else {
|
||||
usageProgress *= usageProgress
|
||||
}
|
||||
fovModifier *= 1 - usageProgress * 0.15
|
||||
}
|
||||
// TODO: Add spyglass/scope check here if needed
|
||||
}
|
||||
|
||||
// Apply FOV effect scale
|
||||
fovModifier = 1 + (fovModifier - 1) * FOV_EFFECT_SCALE
|
||||
|
||||
// Calculate target FOV
|
||||
const baseFov = gameAdditionalState.isZooming ? 30 : options.fov
|
||||
targetFov = baseFov * fovModifier
|
||||
|
||||
// Smooth transition
|
||||
const now = performance.now()
|
||||
if (currentFov !== targetFov) {
|
||||
const elapsed = now - lastUpdateTime
|
||||
const progress = Math.min(elapsed / FOV_TRANSITION_DURATION, 1)
|
||||
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
|
||||
|
||||
currentFov += (targetFov - currentFov) * easeOutCubic(progress)
|
||||
|
||||
if (Math.abs(currentFov - targetFov) < 0.01) {
|
||||
currentFov = targetFov
|
||||
}
|
||||
|
||||
viewer.camera.fov = currentFov
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
}
|
||||
lastUpdateTime = now
|
||||
}
|
||||
|
||||
export const watchFov = () => {
|
||||
const updateFov = () => {
|
||||
if (!bot) return
|
||||
let fov = gameAdditionalState.isZooming ? 30 : options.fov
|
||||
|
||||
if (bot.controlState.sprint && !bot.controlState.sneak) {
|
||||
fov += 5
|
||||
}
|
||||
if (gameAdditionalState.isFlying) {
|
||||
fov += 5
|
||||
}
|
||||
|
||||
if (targetFov !== fov) {
|
||||
targetFov = fov
|
||||
lastUpdateTime = performance.now()
|
||||
}
|
||||
}
|
||||
|
||||
customEvents.on('gameLoaded', () => {
|
||||
updateFov()
|
||||
})
|
||||
|
||||
updateFov()
|
||||
|
||||
// Add FOV animation to render loop
|
||||
// Initial FOV setup
|
||||
if (!beforeRenderFrame.includes(updateFovAnimation)) {
|
||||
beforeRenderFrame.push(updateFovAnimation)
|
||||
}
|
||||
|
||||
subscribeKey(options, 'fov', updateFov)
|
||||
subscribeKey(gameAdditionalState, 'isFlying', updateFov)
|
||||
subscribeKey(gameAdditionalState, 'isSprinting', updateFov)
|
||||
subscribeKey(gameAdditionalState, 'isZooming', updateFov)
|
||||
customEvents.on('gameLoaded', () => {
|
||||
updateFovAnimation()
|
||||
})
|
||||
|
||||
subscribeKey(gameAdditionalState, 'isSneaking', () => {
|
||||
viewer.isSneaking = gameAdditionalState.isSneaking
|
||||
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { proxy } from 'valtio'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { useMedia } from 'react-use'
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import { useScale } from './react/UIProvider'
|
||||
|
||||
export const currentScaling = proxy({
|
||||
scale: 1,
|
||||
|
|
@ -9,11 +11,11 @@ window.currentScaling = currentScaling
|
|||
|
||||
const setScale = () => {
|
||||
const scaleValues = [
|
||||
{ maxWidth: 971, maxHeight: null, scale: 2 },
|
||||
{ maxWidth: 980, maxHeight: null, scale: 2 },
|
||||
{ maxWidth: null, maxHeight: 390, scale: 1.5 }, // todo allow to set the scaling at 360-400 (dynamic scaling setting)
|
||||
{ maxWidth: 590, maxHeight: null, scale: 1 },
|
||||
{ maxWidth: 620, maxHeight: null, scale: 1 },
|
||||
|
||||
{ maxWidth: 590, minHeight: 240, scale: 1.4 },
|
||||
{ maxWidth: 620, minHeight: 240, scale: 1.4 },
|
||||
]
|
||||
|
||||
const { innerWidth, innerHeight } = window
|
||||
|
|
@ -35,3 +37,7 @@ watchValue(currentScaling, (c) => {
|
|||
document.documentElement.style.setProperty('--guiScale', String(c.scale))
|
||||
})
|
||||
window.addEventListener('resize', setScale)
|
||||
|
||||
export const useAppScale = () => {
|
||||
return useSnapshot(currentScaling).scale
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,24 @@ module.exports.resolveSrv = function (hostname, callback) {
|
|||
Http.responseType = 'json'
|
||||
Http.send()
|
||||
|
||||
const minecraftServerHostname = hostname.startsWith('_minecraft._tcp.') ? hostname.slice('_minecraft._tcp.'.length) : null
|
||||
if (minecraftServerHostname) {
|
||||
Http.onerror = async function () {
|
||||
try {
|
||||
if (!globalThis.resolveDnsFallback) return
|
||||
const result = await globalThis.resolveDnsFallback(minecraftServerHostname)
|
||||
callback(null, result ? [{
|
||||
priority: 0,
|
||||
weight: 0,
|
||||
port: result.port,
|
||||
name: result.host
|
||||
}] : [])
|
||||
} catch (err) {
|
||||
callback(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Http.onload = function () {
|
||||
const { response } = Http
|
||||
if (response.Status === 3) {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ export const watchOptionsAfterWorldViewInit = () => {
|
|||
watchValue(options, o => {
|
||||
if (!worldView) return
|
||||
worldView.keepChunksDistance = o.keepChunksDistance
|
||||
viewer.world.config.displayHand = o.handDisplay
|
||||
viewer.world.config.showHand = o.showHand
|
||||
viewer.world.config.viewBobbing = o.viewBobbing
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { options } from './optionsStorage'
|
|||
import { itemBeingUsed } from './react/Crosshair'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
import { displayClientChat } from './botUtils'
|
||||
import { playerState } from './mineflayer/playerState'
|
||||
|
||||
function getViewDirection (pitch, yaw) {
|
||||
const csPitch = Math.cos(pitch)
|
||||
|
|
@ -332,6 +333,7 @@ class WorldInteraction {
|
|||
if (item) {
|
||||
customEvents.emit('activateItem', item, offhand ? 45 : bot.quickBarSlot, offhand)
|
||||
}
|
||||
playerState.startUsingItem()
|
||||
itemBeingUsed.name = (offhand ? bot.inventory.slots[45]?.name : bot.heldItem?.name) ?? null
|
||||
itemBeingUsed.hand = offhand ? 1 : 0
|
||||
}
|
||||
|
|
@ -343,6 +345,7 @@ class WorldInteraction {
|
|||
// "only foods and bow can be deactivated" - not true, shields also can be deactivated and client always sends this
|
||||
// if (bot.heldItem && (loadedData.foodsArray.map((f) => f.name).includes(bot.heldItem.name) || bot.heldItem.name === 'bow')) {
|
||||
bot.deactivateItem()
|
||||
playerState.stopUsingItem()
|
||||
// }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue