Compare commits
13 commits
fog-adjust
...
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253e094c74 | ||
|
|
fef94f03fb | ||
|
|
e9f91f8ecd | ||
|
|
634df8d03d |
||
|
|
a88c8b5470 | ||
|
|
f51254d97a | ||
|
|
05cd560d6b | ||
|
|
b239636356 | ||
|
|
4f421ae45f | ||
|
|
3b94889bed |
||
|
|
636a7fdb54 |
||
|
|
c930365e32 | ||
|
|
852dd737ae |
23 changed files with 277 additions and 46 deletions
|
|
@ -78,6 +78,8 @@ There is a builtin proxy, but you can also host your one! Just clone the repo, r
|
||||||
|
|
||||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
||||||
|
|
||||||
|
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
|
||||||
|
|
||||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@
|
||||||
{
|
{
|
||||||
"ip": "wss://play.mcraft.fun"
|
"ip": "wss://play.mcraft.fun"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ip": "wss://play.webmc.fun",
|
||||||
|
"name": "WebMC"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ip": "wss://ws.fuchsmc.net"
|
"ip": "wss://ws.fuchsmc.net"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
|
import { DebugGui } from '../lib/DebugGui'
|
||||||
|
|
||||||
export const DEFAULT_TEMPERATURE = 0.75
|
export const DEFAULT_TEMPERATURE = 0.75
|
||||||
|
|
||||||
|
|
@ -17,11 +18,33 @@ export class SkyboxRenderer {
|
||||||
private waterBreathing = false
|
private waterBreathing = false
|
||||||
private fogBrightness = 0
|
private fogBrightness = 0
|
||||||
private prevFogBrightness = 0
|
private prevFogBrightness = 0
|
||||||
|
private readonly fogOrangeness = 0 // Debug property to control sky color orangeness
|
||||||
|
private readonly distanceFactor = 2.7
|
||||||
|
|
||||||
|
private readonly brightnessAtPosition = 1
|
||||||
|
debugGui: DebugGui
|
||||||
|
|
||||||
constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) {
|
constructor (private readonly scene: THREE.Scene, public defaultSkybox: boolean, public initialImage: string | null) {
|
||||||
|
this.debugGui = new DebugGui('skybox_renderer', this, [
|
||||||
|
'temperature',
|
||||||
|
'worldTime',
|
||||||
|
'inWater',
|
||||||
|
'waterBreathing',
|
||||||
|
'fogOrangeness',
|
||||||
|
'brightnessAtPosition',
|
||||||
|
'distanceFactor'
|
||||||
|
], {
|
||||||
|
brightnessAtPosition: { min: 0, max: 1, step: 0.01 },
|
||||||
|
temperature: { min: 0, max: 1, step: 0.01 },
|
||||||
|
worldTime: { min: 0, max: 24_000, step: 1 },
|
||||||
|
fogOrangeness: { min: -1, max: 1, step: 0.01 },
|
||||||
|
distanceFactor: { min: 0, max: 5, step: 0.01 },
|
||||||
|
})
|
||||||
|
|
||||||
if (!initialImage) {
|
if (!initialImage) {
|
||||||
this.createGradientSky()
|
this.createGradientSky()
|
||||||
}
|
}
|
||||||
|
// this.debugGui.activate()
|
||||||
}
|
}
|
||||||
|
|
||||||
async init () {
|
async init () {
|
||||||
|
|
@ -95,6 +118,7 @@ export class SkyboxRenderer {
|
||||||
|
|
||||||
// Update world time
|
// Update world time
|
||||||
updateTime (timeOfDay: number, partialTicks = 0) {
|
updateTime (timeOfDay: number, partialTicks = 0) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
this.worldTime = timeOfDay
|
this.worldTime = timeOfDay
|
||||||
this.partialTicks = partialTicks
|
this.partialTicks = partialTicks
|
||||||
this.updateSkyColors()
|
this.updateSkyColors()
|
||||||
|
|
@ -108,12 +132,14 @@ export class SkyboxRenderer {
|
||||||
|
|
||||||
// Update temperature (for biome support)
|
// Update temperature (for biome support)
|
||||||
updateTemperature (temperature: number) {
|
updateTemperature (temperature: number) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
this.temperature = temperature
|
this.temperature = temperature
|
||||||
this.updateSkyColors()
|
this.updateSkyColors()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update water state
|
// Update water state
|
||||||
updateWaterState (inWater: boolean, waterBreathing: boolean) {
|
updateWaterState (inWater: boolean, waterBreathing: boolean) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
this.inWater = inWater
|
this.inWater = inWater
|
||||||
this.waterBreathing = waterBreathing
|
this.waterBreathing = waterBreathing
|
||||||
this.updateSkyColors()
|
this.updateSkyColors()
|
||||||
|
|
@ -121,6 +147,7 @@ export class SkyboxRenderer {
|
||||||
|
|
||||||
// Update default skybox setting
|
// Update default skybox setting
|
||||||
updateDefaultSkybox (defaultSkybox: boolean) {
|
updateDefaultSkybox (defaultSkybox: boolean) {
|
||||||
|
if (this.debugGui.visible) return
|
||||||
this.defaultSkybox = defaultSkybox
|
this.defaultSkybox = defaultSkybox
|
||||||
this.updateSkyColors()
|
this.updateSkyColors()
|
||||||
}
|
}
|
||||||
|
|
@ -229,8 +256,15 @@ export class SkyboxRenderer {
|
||||||
if (temperature < -1) temperature = -1
|
if (temperature < -1) temperature = -1
|
||||||
if (temperature > 1) temperature = 1
|
if (temperature > 1) temperature = 1
|
||||||
|
|
||||||
const hue = 0.622_222_2 - temperature * 0.05
|
// Apply debug fog orangeness to hue - positive values make it more orange, negative make it less orange
|
||||||
const saturation = 0.5 + temperature * 0.1
|
const baseHue = 0.622_222_2 - temperature * 0.05
|
||||||
|
// Orange is around hue 0.08-0.15, so we need to shift from blue-purple (0.62) toward orange
|
||||||
|
// Use a more dramatic shift and also increase saturation for more noticeable effect
|
||||||
|
const orangeHue = 0.12 // Orange hue value
|
||||||
|
const hue = this.fogOrangeness > 0
|
||||||
|
? baseHue + (orangeHue - baseHue) * this.fogOrangeness * 0.8 // Blend toward orange
|
||||||
|
: baseHue + this.fogOrangeness * 0.1 // Subtle shift for negative values
|
||||||
|
const saturation = 0.5 + temperature * 0.1 + Math.abs(this.fogOrangeness) * 0.3 // Increase saturation with orangeness
|
||||||
const brightness = 1
|
const brightness = 1
|
||||||
|
|
||||||
return this.hsbToRgb(hue, saturation, brightness)
|
return this.hsbToRgb(hue, saturation, brightness)
|
||||||
|
|
@ -305,8 +339,7 @@ export class SkyboxRenderer {
|
||||||
// Update fog brightness with smooth transition
|
// Update fog brightness with smooth transition
|
||||||
this.prevFogBrightness = this.fogBrightness
|
this.prevFogBrightness = this.fogBrightness
|
||||||
const renderDistance = this.viewDistance / 32
|
const renderDistance = this.viewDistance / 32
|
||||||
const brightnessAtPosition = 1 // Could be affected by light level in future
|
const targetBrightness = this.brightnessAtPosition * (1 - renderDistance) + renderDistance
|
||||||
const targetBrightness = brightnessAtPosition * (1 - renderDistance) + renderDistance
|
|
||||||
this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1
|
this.fogBrightness += (targetBrightness - this.fogBrightness) * 0.1
|
||||||
|
|
||||||
// Handle water fog
|
// Handle water fog
|
||||||
|
|
@ -340,7 +373,7 @@ export class SkyboxRenderer {
|
||||||
const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness
|
const blue = (fogColor.z + (skyColor.z - fogColor.z) * viewFactor) * clampedBrightness * interpolatedBrightness
|
||||||
|
|
||||||
this.scene.background = new THREE.Color(red, green, blue)
|
this.scene.background = new THREE.Color(red, green, blue)
|
||||||
this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * 2)
|
this.scene.fog = new THREE.Fog(new THREE.Color(red, green, blue), 0.0025, viewDistance * this.distanceFactor)
|
||||||
|
|
||||||
;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z))
|
;(this.skyMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(skyColor.x, skyColor.y, skyColor.z))
|
||||||
;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(
|
;(this.voidMesh.material as THREE.MeshBasicMaterial).color.set(new THREE.Color(
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const WAYPOINT_CONFIG = {
|
||||||
CANVAS_SCALE: 2,
|
CANVAS_SCALE: 2,
|
||||||
ARROW: {
|
ARROW: {
|
||||||
enabledDefault: false,
|
enabledDefault: false,
|
||||||
pixelSize: 30,
|
pixelSize: 50,
|
||||||
paddingPx: 50,
|
paddingPx: 50,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +50,7 @@ export function createWaypointSprite (options: {
|
||||||
depthTest?: boolean,
|
depthTest?: boolean,
|
||||||
// Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this)
|
// Y offset in world units used by updateScaleWorld only (screen-pixel API ignores this)
|
||||||
labelYOffset?: number,
|
labelYOffset?: number,
|
||||||
|
metadata?: any,
|
||||||
}): WaypointSprite {
|
}): WaypointSprite {
|
||||||
const color = options.color ?? 0xFF_00_00
|
const color = options.color ?? 0xFF_00_00
|
||||||
const depthTest = options.depthTest ?? false
|
const depthTest = options.depthTest ?? false
|
||||||
|
|
@ -131,16 +132,22 @@ export function createWaypointSprite (options: {
|
||||||
canvas.height = size
|
canvas.height = size
|
||||||
const ctx = canvas.getContext('2d')!
|
const ctx = canvas.getContext('2d')!
|
||||||
ctx.clearRect(0, 0, size, size)
|
ctx.clearRect(0, 0, size, size)
|
||||||
|
|
||||||
|
// Draw arrow shape
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
ctx.moveTo(size * 0.2, size * 0.5)
|
ctx.moveTo(size * 0.15, size * 0.5)
|
||||||
ctx.lineTo(size * 0.8, size * 0.5)
|
ctx.lineTo(size * 0.85, size * 0.5)
|
||||||
ctx.lineTo(size * 0.5, size * 0.2)
|
ctx.lineTo(size * 0.5, size * 0.15)
|
||||||
ctx.closePath()
|
ctx.closePath()
|
||||||
ctx.lineWidth = 4
|
|
||||||
|
// Use waypoint color for arrow
|
||||||
|
const colorHex = `#${color.toString(16).padStart(6, '0')}`
|
||||||
|
ctx.lineWidth = 6
|
||||||
ctx.strokeStyle = 'black'
|
ctx.strokeStyle = 'black'
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
ctx.fillStyle = 'white'
|
ctx.fillStyle = colorHex
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
|
||||||
const texture = new THREE.CanvasTexture(canvas)
|
const texture = new THREE.CanvasTexture(canvas)
|
||||||
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false })
|
const material = new THREE.SpriteMaterial({ map: texture, transparent: true, depthTest: false, depthWrite: false })
|
||||||
arrowSprite = new THREE.Sprite(material)
|
arrowSprite = new THREE.Sprite(material)
|
||||||
|
|
@ -169,6 +176,9 @@ export function createWaypointSprite (options: {
|
||||||
ensureArrow()
|
ensureArrow()
|
||||||
if (!arrowSprite) return true
|
if (!arrowSprite) return true
|
||||||
|
|
||||||
|
// Check if onlyLeftRight is enabled in metadata
|
||||||
|
const onlyLeftRight = options.metadata?.onlyLeftRight === true
|
||||||
|
|
||||||
// Build camera basis using camera.up to respect custom orientations
|
// Build camera basis using camera.up to respect custom orientations
|
||||||
const forward = new THREE.Vector3()
|
const forward = new THREE.Vector3()
|
||||||
camera.getWorldDirection(forward) // camera look direction
|
camera.getWorldDirection(forward) // camera look direction
|
||||||
|
|
@ -213,6 +223,20 @@ export function createWaypointSprite (options: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply onlyLeftRight logic - restrict arrows to left/right edges only
|
||||||
|
if (onlyLeftRight) {
|
||||||
|
// Force the arrow to appear only on left or right edges
|
||||||
|
if (Math.abs(rx) > Math.abs(ry)) {
|
||||||
|
// Horizontal direction is dominant, keep it
|
||||||
|
ry = 0
|
||||||
|
} else {
|
||||||
|
// Vertical direction is dominant, but we want only left/right
|
||||||
|
// So choose left or right based on the sign of rx
|
||||||
|
rx = rx >= 0 ? 1 : -1
|
||||||
|
ry = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Place on the rectangle border [-1,1]x[-1,1]
|
// Place on the rectangle border [-1,1]x[-1,1]
|
||||||
const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1
|
const s = Math.max(Math.abs(rx), Math.abs(ry)) || 1
|
||||||
let ndcX = rx / s
|
let ndcX = rx / s
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ interface WaypointOptions {
|
||||||
color?: number
|
color?: number
|
||||||
label?: string
|
label?: string
|
||||||
minDistance?: number
|
minDistance?: number
|
||||||
|
metadata?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WaypointsRenderer {
|
export class WaypointsRenderer {
|
||||||
|
|
@ -71,13 +72,14 @@ export class WaypointsRenderer {
|
||||||
this.removeWaypoint(id)
|
this.removeWaypoint(id)
|
||||||
|
|
||||||
const color = options.color ?? 0xFF_00_00
|
const color = options.color ?? 0xFF_00_00
|
||||||
const { label } = options
|
const { label, metadata } = options
|
||||||
const minDistance = options.minDistance ?? 0
|
const minDistance = options.minDistance ?? 0
|
||||||
|
|
||||||
const sprite = createWaypointSprite({
|
const sprite = createWaypointSprite({
|
||||||
position: new THREE.Vector3(x, y, z),
|
position: new THREE.Vector3(x, y, z),
|
||||||
color,
|
color,
|
||||||
label: (label || id),
|
label: (label || id),
|
||||||
|
metadata,
|
||||||
})
|
})
|
||||||
sprite.enableOffscreenArrow(true)
|
sprite.enableOffscreenArrow(true)
|
||||||
sprite.setArrowParent(this.waypointScene)
|
sprite.setArrowParent(this.waypointScene)
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export type AppConfig = {
|
||||||
// defaultVersion?: string
|
// defaultVersion?: string
|
||||||
peerJsServer?: string
|
peerJsServer?: string
|
||||||
peerJsServerFallback?: string
|
peerJsServerFallback?: string
|
||||||
promoteServers?: Array<{ ip, description, version? }>
|
promoteServers?: Array<{ ip, description, name?, version?, }>
|
||||||
mapsProvider?: string
|
mapsProvider?: string
|
||||||
|
|
||||||
appParams?: Record<string, any> // query string params
|
appParams?: Record<string, any> // query string params
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,12 @@ let audioContext: AudioContext
|
||||||
const sounds: Record<string, any> = {}
|
const sounds: Record<string, any> = {}
|
||||||
|
|
||||||
// Track currently playing sounds and their gain nodes
|
// Track currently playing sounds and their gain nodes
|
||||||
const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
|
const activeSounds: Array<{
|
||||||
|
source: AudioBufferSourceNode;
|
||||||
|
gainNode: GainNode;
|
||||||
|
volumeMultiplier: number;
|
||||||
|
isMusic: boolean;
|
||||||
|
}> = []
|
||||||
window.activeSounds = activeSounds
|
window.activeSounds = activeSounds
|
||||||
|
|
||||||
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
||||||
|
|
@ -43,7 +48,7 @@ export async function loadSound (path: string, contents = path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false) => {
|
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = options.remoteSoundsLoadTimeout, loop = false, isMusic = false) => {
|
||||||
const soundBuffer = sounds[url]
|
const soundBuffer = sounds[url]
|
||||||
if (!soundBuffer) {
|
if (!soundBuffer) {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
|
|
@ -51,11 +56,11 @@ export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = option
|
||||||
if (cancelled || Date.now() - start > loadTimeout) return
|
if (cancelled || Date.now() - start > loadTimeout) return
|
||||||
}
|
}
|
||||||
|
|
||||||
return playSound(url, soundVolume, loop)
|
return playSound(url, soundVolume, loop, isMusic)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function playSound (url, soundVolume = 1, loop = false) {
|
export async function playSound (url, soundVolume = 1, loop = false, isMusic = false) {
|
||||||
const volume = soundVolume * (options.volume / 100)
|
const volume = soundVolume * (options.volume / 100) * (isMusic ? options.musicVolume / 100 : 1)
|
||||||
|
|
||||||
if (!volume) return
|
if (!volume) return
|
||||||
|
|
||||||
|
|
@ -82,7 +87,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
|
||||||
source.start(0)
|
source.start(0)
|
||||||
|
|
||||||
// Add to active sounds
|
// Add to active sounds
|
||||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
|
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume, isMusic })
|
||||||
|
|
||||||
const callbacks = [] as Array<() => void>
|
const callbacks = [] as Array<() => void>
|
||||||
source.onended = () => {
|
source.onended = () => {
|
||||||
|
|
@ -110,6 +115,7 @@ export async function playSound (url, soundVolume = 1, loop = false) {
|
||||||
console.warn('Failed to stop sound:', err)
|
console.warn('Failed to stop sound:', err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
gainNode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,11 +143,11 @@ export function stopSound (url: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number, newMusicVolume: number) {
|
||||||
const normalizedVolume = newVolume / 100
|
const normalizedVolume = newVolume / 100
|
||||||
for (const { gainNode, volumeMultiplier } of activeSounds) {
|
for (const { gainNode, volumeMultiplier, isMusic } of activeSounds) {
|
||||||
try {
|
try {
|
||||||
gainNode.gain.value = normalizedVolume * volumeMultiplier
|
gainNode.gain.value = normalizedVolume * volumeMultiplier * (isMusic ? newMusicVolume / 100 : 1)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to change sound volume:', err)
|
console.warn('Failed to change sound volume:', err)
|
||||||
}
|
}
|
||||||
|
|
@ -149,5 +155,9 @@ export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeKey(options, 'volume', () => {
|
subscribeKey(options, 'volume', () => {
|
||||||
changeVolumeOfCurrentlyPlayingSounds(options.volume)
|
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||||
|
})
|
||||||
|
|
||||||
|
subscribeKey(options, 'musicVolume', () => {
|
||||||
|
changeVolumeOfCurrentlyPlayingSounds(options.volume, options.musicVolume)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -82,15 +82,30 @@ const registerWaypointChannels = () => {
|
||||||
{
|
{
|
||||||
name: 'color',
|
name: 'color',
|
||||||
type: 'i32'
|
type: 'i32'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'metadataJson',
|
||||||
|
type: ['pstring', { countType: 'i16' }]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => {
|
registerChannel('minecraft-web-client:waypoint-add', packetStructure, (data) => {
|
||||||
|
// Parse metadata if provided
|
||||||
|
let metadata: any = {}
|
||||||
|
if (data.metadataJson && data.metadataJson.trim() !== '') {
|
||||||
|
try {
|
||||||
|
metadata = JSON.parse(data.metadataJson)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to parse waypoint metadataJson:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, {
|
getThreeJsRendererMethods()?.addWaypoint(data.id, data.x, data.y, data.z, {
|
||||||
minDistance: data.minDistance,
|
minDistance: data.minDistance,
|
||||||
label: data.label || undefined,
|
label: data.label || undefined,
|
||||||
color: data.color || undefined
|
color: data.color || undefined,
|
||||||
|
metadata
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ export const defaultOptions = {
|
||||||
chatOpacityOpened: 100,
|
chatOpacityOpened: 100,
|
||||||
messagesLimit: 200,
|
messagesLimit: 200,
|
||||||
volume: 50,
|
volume: 50,
|
||||||
enableMusic: false,
|
enableMusic: true,
|
||||||
|
musicVolume: 50,
|
||||||
// fov: 70,
|
// fov: 70,
|
||||||
fov: 75,
|
fov: 75,
|
||||||
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
|
defaultPerspective: 'first_person' as 'first_person' | 'third_person_back' | 'third_person_front',
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,17 @@ import { WorldRendererThree } from 'renderer/viewer/three/worldrendererThree'
|
||||||
import { enable, disable, enabled } from 'debug'
|
import { enable, disable, enabled } from 'debug'
|
||||||
import { Vec3 } from 'vec3'
|
import { Vec3 } from 'vec3'
|
||||||
|
|
||||||
|
customEvents.on('mineflayerBotCreated', () => {
|
||||||
|
window.debugServerPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toClient.types).map(name => {
|
||||||
|
name = name.replace('packet_', '')
|
||||||
|
return [name, name]
|
||||||
|
}))
|
||||||
|
window.debugClientPacketNames = Object.fromEntries(Object.keys(loadedData.protocol.play.toServer.types).map(name => {
|
||||||
|
name = name.replace('packet_', '')
|
||||||
|
return [name, name]
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
window.Vec3 = Vec3
|
window.Vec3 = Vec3
|
||||||
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
window.cursorBlockRel = (x = 0, y = 0, z = 0) => {
|
||||||
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
|
const newPos = bot.blockAtCursor(5)?.position.offset(x, y, z)
|
||||||
|
|
|
||||||
|
|
@ -246,22 +246,29 @@ customEvents.on('gameLoaded', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// even if not found, still record to cache
|
// even if not found, still record to cache
|
||||||
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
void getThreeJsRendererMethods()!.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error decoding player texture:', err)
|
reportError(new Error('Error applying skin texture:', { cause: err }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.on('playerJoined', updateSkin)
|
bot.on('playerJoined', updateSkin)
|
||||||
bot.on('playerUpdated', updateSkin)
|
bot.on('playerUpdated', updateSkin)
|
||||||
|
for (const entity of Object.values(bot.players)) {
|
||||||
|
updateSkin(entity)
|
||||||
|
}
|
||||||
|
|
||||||
bot.on('teamUpdated', (team: Team) => {
|
const teamUpdated = (team: Team) => {
|
||||||
for (const entity of Object.values(bot.entities)) {
|
for (const entity of Object.values(bot.entities)) {
|
||||||
if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
|
if (entity.type === 'player' && entity.username && team.members.includes(entity.username) || entity.uuid && team.members.includes(entity.uuid)) {
|
||||||
bot.emit('entityUpdate', entity)
|
bot.emit('entityUpdate', entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
bot.on('teamUpdated', teamUpdated)
|
||||||
|
for (const team of Object.values(bot.teams)) {
|
||||||
|
teamUpdated(team)
|
||||||
|
}
|
||||||
|
|
||||||
const updateEntityNameTags = (team: Team) => {
|
const updateEntityNameTags = (team: Team) => {
|
||||||
for (const entity of Object.values(bot.entities)) {
|
for (const entity of Object.values(bot.entities)) {
|
||||||
|
|
|
||||||
|
|
@ -470,6 +470,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
|
||||||
const isRightClick = type === 'rightclick'
|
const isRightClick = type === 'rightclick'
|
||||||
const isLeftClick = type === 'leftclick'
|
const isLeftClick = type === 'leftclick'
|
||||||
if (isLeftClick || isRightClick) {
|
if (isLeftClick || isRightClick) {
|
||||||
|
modelViewerState.model = undefined
|
||||||
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
|
inv.canvasManager.children[0].showRecipesOrUsages(isLeftClick, item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -501,6 +502,7 @@ const openWindow = (type: string | undefined, title: string | any = undefined) =
|
||||||
if (freeSlot === null) return
|
if (freeSlot === null) return
|
||||||
void bot.creative.setInventorySlot(freeSlot, item)
|
void bot.creative.setInventorySlot(freeSlot, item)
|
||||||
} else {
|
} else {
|
||||||
|
modelViewerState.model = undefined
|
||||||
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0])
|
inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,12 @@ class CustomDuplex extends Duplex {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getWebsocketStream = async (host: string) => {
|
export const getWebsocketStream = async (host: string) => {
|
||||||
const baseProtocol = location.protocol === 'https:' ? 'wss' : host.startsWith('ws://') ? 'ws' : 'wss'
|
const baseProtocol = host.startsWith('ws://') ? 'ws' : 'wss'
|
||||||
const hostClean = host.replace('ws://', '').replace('wss://', '')
|
const hostClean = host.replace('ws://', '').replace('wss://', '')
|
||||||
const ws = new WebSocket(`${baseProtocol}://${hostClean}`)
|
const hostURL = new URL(`${baseProtocol}://${hostClean}`)
|
||||||
|
const hostParams = hostURL.searchParams
|
||||||
|
hostParams.append('client_mcraft', '')
|
||||||
|
const ws = new WebSocket(`${baseProtocol}://${hostURL.host}${hostURL.pathname}?${hostParams.toString()}`)
|
||||||
const clientDuplex = new CustomDuplex(undefined, data => {
|
const clientDuplex = new CustomDuplex(undefined, data => {
|
||||||
ws.send(data)
|
ws.send(data)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,24 @@ export const guiOptionsScheme: {
|
||||||
],
|
],
|
||||||
sound: [
|
sound: [
|
||||||
{ volume: {} },
|
{ volume: {} },
|
||||||
|
{
|
||||||
|
custom () {
|
||||||
|
return <OptionSlider
|
||||||
|
valueOverride={options.enableMusic ? undefined : 0}
|
||||||
|
onChange={(value) => {
|
||||||
|
options.musicVolume = value
|
||||||
|
}}
|
||||||
|
item={{
|
||||||
|
type: 'slider',
|
||||||
|
id: 'musicVolume',
|
||||||
|
text: 'Music Volume',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
unit: '%',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
custom () {
|
custom () {
|
||||||
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
|
return <Button label='Sound Muffler' onClick={() => showModal({ reactType: 'sound-muffler' })} inScreen />
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export const startLocalReplayServer = (contents: string) => {
|
||||||
const server = createServer({
|
const server = createServer({
|
||||||
Server: LocalServer as any,
|
Server: LocalServer as any,
|
||||||
version: header.minecraftVersion,
|
version: header.minecraftVersion,
|
||||||
|
keepAlive: false,
|
||||||
'online-mode': false
|
'online-mode': false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayConnectButton = qsParamIp
|
const displayConnectButton = qsParamIp
|
||||||
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg']
|
const serverExamples = ['example.com:25565', 'play.hypixel.net', 'ws://play.pcm.gg', 'wss://play.webmc.fun']
|
||||||
// pick random example
|
// pick random example
|
||||||
const example = serverExamples[Math.floor(Math.random() * serverExamples.length)]
|
const example = serverExamples[Math.floor(Math.random() * serverExamples.length)]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,9 @@ export default ({
|
||||||
const chatInput = useRef<HTMLInputElement>(null!)
|
const chatInput = useRef<HTMLInputElement>(null!)
|
||||||
const chatMessages = useRef<HTMLDivElement>(null)
|
const chatMessages = useRef<HTMLDivElement>(null)
|
||||||
const chatHistoryPos = useRef(sendHistoryRef.current.length)
|
const chatHistoryPos = useRef(sendHistoryRef.current.length)
|
||||||
|
const commandHistoryPos = useRef(0)
|
||||||
const inputCurrentlyEnteredValue = useRef('')
|
const inputCurrentlyEnteredValue = useRef('')
|
||||||
|
const commandHistoryRef = useRef(sendHistoryRef.current.filter((msg: string) => msg.startsWith('/')))
|
||||||
|
|
||||||
const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened })
|
const { scrollToBottom, isAtBottom, wasAtBottom, currentlyAtBottom } = useScrollBehavior(chatMessages, { messages, opened })
|
||||||
const [rightNowAtBottom, setRightNowAtBottom] = useState(false)
|
const [rightNowAtBottom, setRightNowAtBottom] = useState(false)
|
||||||
|
|
@ -142,6 +144,9 @@ export default ({
|
||||||
sendHistoryRef.current = newHistory
|
sendHistoryRef.current = newHistory
|
||||||
window.sessionStorage.chatHistory = JSON.stringify(newHistory)
|
window.sessionStorage.chatHistory = JSON.stringify(newHistory)
|
||||||
chatHistoryPos.current = newHistory.length
|
chatHistoryPos.current = newHistory.length
|
||||||
|
// Update command history (only messages starting with /)
|
||||||
|
commandHistoryRef.current = newHistory.filter((msg: string) => msg.startsWith('/'))
|
||||||
|
commandHistoryPos.current = commandHistoryRef.current.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const acceptComplete = (item: string) => {
|
const acceptComplete = (item: string) => {
|
||||||
|
|
@ -180,6 +185,21 @@ export default ({
|
||||||
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCommandArrowUp = () => {
|
||||||
|
if (commandHistoryPos.current === 0 || commandHistoryRef.current.length === 0) return
|
||||||
|
if (commandHistoryPos.current === commandHistoryRef.current.length) { // started navigating command history
|
||||||
|
inputCurrentlyEnteredValue.current = chatInput.current.value
|
||||||
|
}
|
||||||
|
commandHistoryPos.current--
|
||||||
|
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCommandArrowDown = () => {
|
||||||
|
if (commandHistoryPos.current === commandHistoryRef.current.length) return
|
||||||
|
commandHistoryPos.current++
|
||||||
|
updateInputValue(commandHistoryRef.current[commandHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||||
|
}
|
||||||
|
|
||||||
const auxInputFocus = (direction: 'up' | 'down') => {
|
const auxInputFocus = (direction: 'up' | 'down') => {
|
||||||
chatInput.current.focus()
|
chatInput.current.focus()
|
||||||
if (direction === 'up') {
|
if (direction === 'up') {
|
||||||
|
|
@ -203,6 +223,7 @@ export default ({
|
||||||
updateInputValue(chatInputValueGlobal.value)
|
updateInputValue(chatInputValueGlobal.value)
|
||||||
chatInputValueGlobal.value = ''
|
chatInputValueGlobal.value = ''
|
||||||
chatHistoryPos.current = sendHistoryRef.current.length
|
chatHistoryPos.current = sendHistoryRef.current.length
|
||||||
|
commandHistoryPos.current = commandHistoryRef.current.length
|
||||||
if (!usingTouch) {
|
if (!usingTouch) {
|
||||||
chatInput.current.focus()
|
chatInput.current.focus()
|
||||||
}
|
}
|
||||||
|
|
@ -524,9 +545,19 @@ export default ({
|
||||||
onBlur={() => setIsInputFocused(false)}
|
onBlur={() => setIsInputFocused(false)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.code === 'ArrowUp') {
|
if (e.code === 'ArrowUp') {
|
||||||
handleArrowUp()
|
if (e.altKey) {
|
||||||
|
handleCommandArrowUp()
|
||||||
|
e.preventDefault()
|
||||||
|
} else {
|
||||||
|
handleArrowUp()
|
||||||
|
}
|
||||||
} else if (e.code === 'ArrowDown') {
|
} else if (e.code === 'ArrowDown') {
|
||||||
handleArrowDown()
|
if (e.altKey) {
|
||||||
|
handleCommandArrowDown()
|
||||||
|
e.preventDefault()
|
||||||
|
} else {
|
||||||
|
handleArrowDown()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (e.code === 'Tab') {
|
if (e.code === 'Tab') {
|
||||||
if (completionItemsSource.length) {
|
if (completionItemsSource.length) {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,15 @@ export const OptionButton = ({ item, onClick, valueText, cacheKey }: {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
|
export const OptionSlider = ({
|
||||||
|
item,
|
||||||
|
onChange,
|
||||||
|
valueOverride
|
||||||
|
}: {
|
||||||
|
item: Extract<OptionMeta, { type: 'slider' }>
|
||||||
|
onChange?: (value: number) => void
|
||||||
|
valueOverride?: number
|
||||||
|
}) => {
|
||||||
const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
|
const { disabledBecauseOfSetting } = useCommonComponentsProps(item)
|
||||||
|
|
||||||
const optionValue = useSnapshot(options)[item.id!]
|
const optionValue = useSnapshot(options)[item.id!]
|
||||||
|
|
@ -174,7 +182,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
|
||||||
return (
|
return (
|
||||||
<Slider
|
<Slider
|
||||||
label={item.text!}
|
label={item.text!}
|
||||||
value={options[item.id!]}
|
value={valueOverride ?? options[item.id!]}
|
||||||
data-setting={item.id}
|
data-setting={item.id}
|
||||||
disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason}
|
disabledReason={isLocked(item) ? 'qs' : disabledBecauseOfSetting ? `Disabled because ${item.disableIf![0]} is ${item.disableIf![1]}` : item.disabledReason}
|
||||||
min={item.min}
|
min={item.min}
|
||||||
|
|
@ -184,6 +192,7 @@ export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slid
|
||||||
updateOnDragEnd={item.delayApply}
|
updateOnDragEnd={item.delayApply}
|
||||||
updateValue={(value) => {
|
updateValue={(value) => {
|
||||||
options[item.id!] = value
|
options[item.id!] = value
|
||||||
|
onChange?.(value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
|
||||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
|
||||||
import { applySkinToPlayerObject, createPlayerObject, PlayerObjectType } from '../../renderer/viewer/lib/createPlayerObject'
|
import { applySkinToPlayerObject, createPlayerObject, PlayerObjectType } from '../../renderer/viewer/lib/createPlayerObject'
|
||||||
import { currentScaling } from '../scaleInterface'
|
import { currentScaling } from '../scaleInterface'
|
||||||
|
import { activeModalStack } from '../globalState'
|
||||||
|
|
||||||
THREE.ColorManagement.enabled = false
|
THREE.ColorManagement.enabled = false
|
||||||
|
|
||||||
|
|
@ -29,6 +30,7 @@ export const modelViewerState = proxy({
|
||||||
modelCustomization?: { [modelUrl: string]: { color?: string, opacity?: number, metalness?: number, roughness?: number } }
|
modelCustomization?: { [modelUrl: string]: { color?: string, opacity?: number, metalness?: number, roughness?: number } }
|
||||||
resetRotationOnReleae?: boolean
|
resetRotationOnReleae?: boolean
|
||||||
continiousRender?: boolean
|
continiousRender?: boolean
|
||||||
|
alwaysRender?: boolean
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
globalThis.modelViewerState = modelViewerState
|
globalThis.modelViewerState = modelViewerState
|
||||||
|
|
@ -75,6 +77,15 @@ globalThis.getModelViewerValues = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subscribe(activeModalStack, () => {
|
||||||
|
if (!modelViewerState.model || !modelViewerState.model?.alwaysRender) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activeModalStack.length === 0) {
|
||||||
|
modelViewerState.model = undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { model } = useSnapshot(modelViewerState)
|
const { model } = useSnapshot(modelViewerState)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
@ -108,11 +119,15 @@ export default () => {
|
||||||
modelLoaders.current.set(modelUrl, loader)
|
modelLoaders.current.set(modelUrl, loader)
|
||||||
|
|
||||||
const onLoad = (object: THREE.Object3D) => {
|
const onLoad = (object: THREE.Object3D) => {
|
||||||
// Apply customization if available
|
// Apply customization if available and enable shadows
|
||||||
const customization = model?.modelCustomization?.[modelUrl]
|
const customization = model?.modelCustomization?.[modelUrl]
|
||||||
if (customization) {
|
object.traverse((child) => {
|
||||||
object.traverse((child) => {
|
if (child instanceof THREE.Mesh) {
|
||||||
if (child instanceof THREE.Mesh && child.material) {
|
// Enable shadow casting and receiving for all meshes
|
||||||
|
child.castShadow = true
|
||||||
|
child.receiveShadow = true
|
||||||
|
|
||||||
|
if (child.material && customization) {
|
||||||
const material = child.material as THREE.MeshStandardMaterial
|
const material = child.material as THREE.MeshStandardMaterial
|
||||||
if (customization.color) {
|
if (customization.color) {
|
||||||
material.color.setHex(parseInt(customization.color.replace('#', ''), 16))
|
material.color.setHex(parseInt(customization.color.replace('#', ''), 16))
|
||||||
|
|
@ -128,8 +143,8 @@ export default () => {
|
||||||
material.roughness = customization.roughness
|
material.roughness = customization.roughness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// Center and scale model
|
// Center and scale model
|
||||||
const box = new THREE.Box3().setFromObject(object)
|
const box = new THREE.Box3().setFromObject(object)
|
||||||
|
|
@ -248,6 +263,12 @@ export default () => {
|
||||||
}
|
}
|
||||||
renderer.setPixelRatio(scale)
|
renderer.setPixelRatio(scale)
|
||||||
renderer.setSize(model.positioning.width, model.positioning.height)
|
renderer.setSize(model.positioning.width, model.positioning.height)
|
||||||
|
|
||||||
|
// Enable shadow rendering for depth and realism
|
||||||
|
renderer.shadowMap.enabled = true
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap // Soft shadows for better quality
|
||||||
|
renderer.shadowMap.autoUpdate = true
|
||||||
|
|
||||||
containerRef.current.appendChild(renderer.domElement)
|
containerRef.current.appendChild(renderer.domElement)
|
||||||
|
|
||||||
// Setup controls
|
// Setup controls
|
||||||
|
|
@ -259,10 +280,30 @@ export default () => {
|
||||||
controls.enableDamping = true
|
controls.enableDamping = true
|
||||||
controls.dampingFactor = 0.05
|
controls.dampingFactor = 0.05
|
||||||
|
|
||||||
// Add ambient light
|
// Add ambient light for overall illumination
|
||||||
const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 1)
|
const ambientLight = new THREE.AmbientLight(0xff_ff_ff, 0.4) // Reduced intensity to allow shadows
|
||||||
scene.add(ambientLight)
|
scene.add(ambientLight)
|
||||||
|
|
||||||
|
// Add directional light for shadows and depth (similar to Minecraft inventory lighting)
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.6)
|
||||||
|
directionalLight.position.set(2, 2, 2) // Position light from top-right-front
|
||||||
|
directionalLight.target.position.set(0, 0, 0) // Point towards center of scene
|
||||||
|
|
||||||
|
// Configure shadow properties for optimal quality
|
||||||
|
directionalLight.castShadow = true
|
||||||
|
directionalLight.shadow.mapSize.width = 2048 // High resolution shadow map
|
||||||
|
directionalLight.shadow.mapSize.height = 2048
|
||||||
|
directionalLight.shadow.camera.near = 0.1
|
||||||
|
directionalLight.shadow.camera.far = 10
|
||||||
|
directionalLight.shadow.camera.left = -3
|
||||||
|
directionalLight.shadow.camera.right = 3
|
||||||
|
directionalLight.shadow.camera.top = 3
|
||||||
|
directionalLight.shadow.camera.bottom = -3
|
||||||
|
directionalLight.shadow.bias = -0.0001 // Reduce shadow acne
|
||||||
|
|
||||||
|
scene.add(directionalLight)
|
||||||
|
scene.add(directionalLight.target)
|
||||||
|
|
||||||
// Cursor following function
|
// Cursor following function
|
||||||
const updatePlayerLookAt = () => {
|
const updatePlayerLookAt = () => {
|
||||||
if (!isFollowingCursor.current || !sceneRef.current?.playerObject) return
|
if (!isFollowingCursor.current || !sceneRef.current?.playerObject) return
|
||||||
|
|
@ -331,6 +372,14 @@ export default () => {
|
||||||
scale: 1 // Start with base scale, will adjust below
|
scale: 1 // Start with base scale, will adjust below
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Enable shadows for player object
|
||||||
|
wrapper.traverse((child) => {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
child.castShadow = true
|
||||||
|
child.receiveShadow = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Calculate proper scale and positioning for camera view
|
// Calculate proper scale and positioning for camera view
|
||||||
const box = new THREE.Box3().setFromObject(wrapper)
|
const box = new THREE.Box3().setFromObject(wrapper)
|
||||||
const size = box.getSize(new THREE.Vector3())
|
const size = box.getSize(new THREE.Vector3())
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
...serversListProvided,
|
...serversListProvided,
|
||||||
...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({
|
...(customServersList ? [] : (miscUiState.appConfig?.promoteServers ?? [])).map((server): StoreServerItem => ({
|
||||||
ip: server.ip,
|
ip: server.ip,
|
||||||
|
name: server.name,
|
||||||
versionOverride: server.version,
|
versionOverride: server.version,
|
||||||
description: server.description,
|
description: server.description,
|
||||||
isRecommended: true
|
isRecommended: true
|
||||||
|
|
@ -167,6 +168,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
||||||
console.log('pingResult.fullInfo.description', pingResult.fullInfo.description)
|
console.log('pingResult.fullInfo.description', pingResult.fullInfo.description)
|
||||||
data = {
|
data = {
|
||||||
formattedText: pingResult.fullInfo.description,
|
formattedText: pingResult.fullInfo.description,
|
||||||
|
icon: pingResult.fullInfo.favicon,
|
||||||
textNameRight: `ws ${pingResult.latency}ms`,
|
textNameRight: `ws ${pingResult.latency}ms`,
|
||||||
textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
|
textNameRightGrayed: `${pingResult.fullInfo.players?.online ?? '??'}/${pingResult.fullInfo.players?.max ?? '??'}`,
|
||||||
offline: false
|
offline: false
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
|
/* Account for GUI scaling */
|
||||||
|
width: calc(100dvw / var(--guiScale, 1));
|
||||||
|
height: calc(100dvh / var(--guiScale, 1));
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screen-content {
|
.screen-content {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ class MusicSystem {
|
||||||
private currentMusic: string | null = null
|
private currentMusic: string | null = null
|
||||||
|
|
||||||
async playMusic (url: string, musicVolume = 1) {
|
async playMusic (url: string, musicVolume = 1) {
|
||||||
if (!options.enableMusic || this.currentMusic) return
|
if (!options.enableMusic || this.currentMusic || options.musicVolume === 0) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { onEnded } = await loadOrPlaySound(url, 0.5 * musicVolume, 5000) ?? {}
|
const { onEnded } = await loadOrPlaySound(url, musicVolume, 5000, undefined, true) ?? {}
|
||||||
|
|
||||||
if (!onEnded) return
|
if (!onEnded) return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { subscribeKey } from 'valtio/utils'
|
import { subscribeKey } from 'valtio/utils'
|
||||||
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
|
import { isMobile } from 'renderer/viewer/lib/simpleUtils'
|
||||||
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
|
import { WorldDataEmitter } from 'renderer/viewer/lib/worldDataEmitter'
|
||||||
|
import { setSkinsConfig } from 'renderer/viewer/lib/utils/skins'
|
||||||
import { options, watchValue } from './optionsStorage'
|
import { options, watchValue } from './optionsStorage'
|
||||||
import { reloadChunks } from './utils'
|
import { reloadChunks } from './utils'
|
||||||
import { miscUiState } from './globalState'
|
import { miscUiState } from './globalState'
|
||||||
|
|
@ -97,6 +98,8 @@ export const watchOptionsAfterViewerInit = () => {
|
||||||
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
|
appViewer.inWorldRenderingConfig.highlightBlockColor = o.highlightBlockColor
|
||||||
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
|
appViewer.inWorldRenderingConfig._experimentalSmoothChunkLoading = o.rendererSharedOptions._experimentalSmoothChunkLoading
|
||||||
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
|
appViewer.inWorldRenderingConfig._renderByChunks = o.rendererSharedOptions._renderByChunks
|
||||||
|
|
||||||
|
setSkinsConfig({ apiEnabled: o.loadPlayerSkins })
|
||||||
})
|
})
|
||||||
|
|
||||||
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
|
appViewer.inWorldRenderingConfig.smoothLighting = options.smoothLighting
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue