Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion renderer/viewer/three/world/vr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { buttonMap as standardButtonsMap } from 'contro-max/build/gamepad'
import * as THREE from 'three'
import { WorldRendererThree } from '../worldrendererThree'
import { DocumentRenderer } from '../documentRenderer'
import { VRHud } from './vrHud'

export async function initVR (worldRenderer: WorldRendererThree, documentRenderer: DocumentRenderer) {
if (!('xr' in navigator) || !worldRenderer.worldRendererConfig.vrSupport) return
Expand All @@ -15,6 +16,9 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere

enableVr()

// Create VR HUD
const vrHud = new VRHud(worldRenderer)

const vrButtonContainer = createVrButtonContainer(renderer)
const updateVrButtons = () => {
const newHidden = !worldRenderer.worldRendererConfig.vrSupport || !worldRenderer.worldRendererConfig.foreground
Expand All @@ -37,6 +41,9 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere
worldRenderer.reactiveState.preventEscapeMenu = false
worldRenderer.scene.remove(user)
vrButtonContainer.hidden = true
// Detach HUD when VR is disabled
vrHud.detachFromVRCamera(user)
vrHud.setVisible(false)
}

function createVrButtonContainer (renderer) {
Expand Down Expand Up @@ -199,16 +206,28 @@ export async function initVR (worldRenderer: WorldRendererThree, documentRendere
// bot.entity.yaw = Math.atan2(-d.x, -d.z)
// bot.entity.pitch = Math.asin(d.y)

// Update VR HUD
vrHud.update()

documentRenderer.frameRender(false)
})
renderer.xr.addEventListener('sessionstart', () => {
worldRenderer.cameraGroupVr = user
// Attach HUD to VR camera when session starts
vrHud.attachToVRCamera(user)
vrHud.setVisible(true)
})
renderer.xr.addEventListener('sessionend', () => {
worldRenderer.cameraGroupVr = undefined
// Detach HUD when session ends
vrHud.detachFromVRCamera(user)
vrHud.setVisible(false)
})

worldRenderer.abortController.signal.addEventListener('abort', disableVr)
worldRenderer.abortController.signal.addEventListener('abort', () => {
disableVr()
vrHud.dispose()
})
}

const xrStandardRightButtonsMap = [
Expand Down
129 changes: 129 additions & 0 deletions renderer/viewer/three/world/vrHud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import * as THREE from 'three'
import { WorldRendererThree } from '../worldrendererThree'

export class VRHud {
private hudMesh: THREE.Mesh
private hudCanvas: HTMLCanvasElement
private hudContext: CanvasRenderingContext2D
private hudTexture: THREE.CanvasTexture
private hudGroup: THREE.Group

Check failure on line 10 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
constructor(private worldRenderer: WorldRendererThree) {

Check failure on line 11 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Missing space before function parentheses
// Create canvas for HUD
this.hudCanvas = document.createElement('canvas')
this.hudCanvas.width = 1024
this.hudCanvas.height = 512

Check failure on line 16 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
this.hudContext = this.hudCanvas.getContext('2d')!
Comment on lines +11 to +17
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for canvas context creation.

The non-null assertion on getContext('2d')! could fail if the context cannot be created, leading to runtime errors.

-    this.hudContext = this.hudCanvas.getContext('2d')!
+    const context = this.hudCanvas.getContext('2d')
+    if (!context) {
+      throw new Error('Failed to create 2D canvas context for VR HUD')
+    }
+    this.hudContext = context
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(private worldRenderer: WorldRendererThree) {
// Create canvas for HUD
this.hudCanvas = document.createElement('canvas')
this.hudCanvas.width = 1024
this.hudCanvas.height = 512
this.hudContext = this.hudCanvas.getContext('2d')!
constructor(private worldRenderer: WorldRendererThree) {
// Create canvas for HUD
this.hudCanvas = document.createElement('canvas')
this.hudCanvas.width = 1024
this.hudCanvas.height = 512
const context = this.hudCanvas.getContext('2d')
if (!context) {
throw new Error('Failed to create 2D canvas context for VR HUD')
}
this.hudContext = context
🧰 Tools
🪛 ESLint

[error] 11-11: Missing space before function parentheses.

(@stylistic/space-before-function-paren)


[error] 16-16: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🤖 Prompt for AI Agents
In renderer/viewer/three/world/vrHud.ts around lines 11 to 17, the code uses a
non-null assertion when getting the 2D context from the canvas, which can cause
runtime errors if the context creation fails. Modify the constructor to check if
the context is null after calling getContext('2d'), and handle the error
appropriately, such as throwing an error or logging a message, to prevent
unexpected failures.


Check failure on line 18 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
// Create texture from canvas
this.hudTexture = new THREE.CanvasTexture(this.hudCanvas)
this.hudTexture.minFilter = THREE.LinearFilter
this.hudTexture.magFilter = THREE.LinearFilter

Check failure on line 23 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
// Create HUD geometry - a plane that will display our canvas
// Adjusted size for better VR viewing
const hudGeometry = new THREE.PlaneGeometry(3, 1.5)
const hudMaterial = new THREE.MeshBasicMaterial({
map: this.hudTexture,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide,
depthTest: false,
depthWrite: false
})

Check failure on line 35 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
this.hudMesh = new THREE.Mesh(hudGeometry, hudMaterial)
this.hudMesh.renderOrder = 1000 // Render on top

Check failure on line 38 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
// Create a group to hold the HUD
this.hudGroup = new THREE.Group()
this.hudGroup.add(this.hudMesh)

Check failure on line 42 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
// Position the HUD in front of the camera
// Slightly lower and further for comfortable VR viewing
this.hudMesh.position.set(0, -0.3, -2.5)

Check failure on line 46 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
// Initial render to show something
this.update()
}

Check failure on line 50 in renderer/viewer/three/world/vrHud.ts

View workflow job for this annotation

GitHub Actions / build-and-deploy

Trailing spaces not allowed
attachToVRCamera(vrCameraGroup: THREE.Object3D) {
// Add HUD to the VR camera group so it follows the player's view
vrCameraGroup.add(this.hudGroup)
}

detachFromVRCamera(vrCameraGroup: THREE.Object3D) {
vrCameraGroup.remove(this.hudGroup)
}

update() {
// Get player data
const bot = (window as any).bot
const playerState = this.worldRenderer.playerState
Comment on lines +62 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve type safety for global bot access.

Accessing (window as any).bot bypasses type safety and could lead to runtime errors if bot is undefined or doesn't have expected properties.

-    const bot = (window as any).bot
-    const playerState = this.worldRenderer.playerState
+    const bot = (globalThis as any).bot
+    const playerState = this.worldRenderer.playerState
+    
+    // Add safety checks for bot existence
+    if (!bot) {
+      return // Skip update if bot is not available
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const bot = (window as any).bot
const playerState = this.worldRenderer.playerState
const bot = (globalThis as any).bot
const playerState = this.worldRenderer.playerState
// Add safety checks for bot existence
if (!bot) {
return // Skip update if bot is not available
}
🧰 Tools
🪛 ESLint

[error] 62-62: Use object destructuring.

(prefer-destructuring)


[error] 63-63: Use object destructuring.

(prefer-destructuring)

🤖 Prompt for AI Agents
In renderer/viewer/three/world/vrHud.ts around lines 62 to 63, the code accesses
the global bot object using (window as any).bot, which bypasses type safety and
risks runtime errors if bot is undefined or malformed. To fix this, define a
proper TypeScript interface for the bot object with expected properties, extend
the Window interface to include this bot type, and then access bot via
window.bot with type safety. Additionally, add a runtime check to ensure bot is
defined before using it to prevent potential errors.


// Clear canvas
this.hudContext.clearRect(0, 0, this.hudCanvas.width, this.hudCanvas.height)

// Set up text styling
this.hudContext.fillStyle = 'white'
this.hudContext.strokeStyle = 'black'
this.hudContext.lineWidth = 3
this.hudContext.font = 'bold 32px Arial'
this.hudContext.textAlign = 'left'
this.hudContext.textBaseline = 'top'

// Top left - FPS and Ping
const fps = Math.round(1000 / this.worldRenderer.renderTimeAvg) || 0
const ping = bot?._client?.latency || 0
Comment on lines +77 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add fallback handling for undefined renderTimeAvg.

The FPS calculation could fail if renderTimeAvg is zero or undefined, leading to division by zero or NaN values.

-    const fps = Math.round(1000 / this.worldRenderer.renderTimeAvg) || 0
+    const fps = this.worldRenderer.renderTimeAvg > 0 
+      ? Math.round(1000 / this.worldRenderer.renderTimeAvg) 
+      : 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const fps = Math.round(1000 / this.worldRenderer.renderTimeAvg) || 0
const ping = bot?._client?.latency || 0
const fps = this.worldRenderer.renderTimeAvg > 0
? Math.round(1000 / this.worldRenderer.renderTimeAvg)
: 0
const ping = bot?._client?.latency || 0
🤖 Prompt for AI Agents
In renderer/viewer/three/world/vrHud.ts around lines 77 to 78, the FPS
calculation does not handle cases where renderTimeAvg is zero or undefined,
which can cause division by zero or result in NaN. Add a fallback check to
ensure renderTimeAvg is a positive number before performing the division; if it
is zero or undefined, set FPS to zero to avoid invalid calculations.


this.drawText(`FPS: ${fps}`, 50, 50)
this.drawText(`Ping: ${ping}ms`, 50, 90)

// Top right - Velocity and Coords
this.hudContext.textAlign = 'right'
const velocity = playerState.getVelocity()
const position = playerState.getPosition()
const vel = Math.sqrt(velocity.x ** 2 + velocity.z ** 2).toFixed(2)

this.drawText(`Vel: ${vel} m/s`, this.hudCanvas.width - 50, 50)
this.drawText(`X: ${position.x.toFixed(1)}`, this.hudCanvas.width - 50, 90)
this.drawText(`Y: ${position.y.toFixed(1)}`, this.hudCanvas.width - 50, 130)
this.drawText(`Z: ${position.z.toFixed(1)}`, this.hudCanvas.width - 50, 170)

// Bottom left - Health
this.hudContext.textAlign = 'left'
this.hudContext.textBaseline = 'bottom'
const health = bot?.health || 10
const maxHealth = 20
Comment on lines +97 to +98
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make health configuration dynamic.

The hard-coded maxHealth = 20 should be retrieved from the game state or configuration.

-    const health = bot?.health || 10
-    const maxHealth = 20
+    const health = bot?.health || 0
+    const maxHealth = bot?.maxHealth || 20
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const health = bot?.health || 10
const maxHealth = 20
const health = bot?.health || 0
const maxHealth = bot?.maxHealth || 20
🤖 Prompt for AI Agents
In renderer/viewer/three/world/vrHud.ts around lines 97 to 98, replace the
hard-coded maxHealth value of 20 with a dynamic value fetched from the game
state or configuration. Identify where the game state or configuration stores
the maximum health value and update the code to retrieve maxHealth from that
source instead of using a fixed number.

const hearts = health / 2
const maxHearts = maxHealth / 2

this.drawText(`HP: ${hearts}/${maxHearts} ❤`, 50, this.hudCanvas.height - 50)

// Bottom right - Game mode
this.hudContext.textAlign = 'right'
const gameMode = playerState.reactive.gameMode || 'survival'
this.drawText(`Mode: ${gameMode}`, this.hudCanvas.width - 50, this.hudCanvas.height - 50)

// Update texture
this.hudTexture.needsUpdate = true
}

private drawText(text: string, x: number, y: number) {
// Draw text with outline for better visibility
this.hudContext.strokeText(text, x, y)
this.hudContext.fillText(text, x, y)
}

setVisible(visible: boolean) {
this.hudMesh.visible = visible
}

dispose() {
this.hudTexture.dispose()
this.hudMesh.geometry.dispose()
;(this.hudMesh.material as THREE.Material).dispose()
this.hudCanvas.remove()
}
}
Loading