pages235/renderer/viewer/three/firstPersonEffects.ts
Cursor Agent 8723f9092f feat: add comprehensive module declarations and enhance type safety
- Added complete module declarations for three, mc-assets, valtio, and three-stdlib
- Enhanced type safety for AtlasParser, TextureInfo, and BlockModel interfaces
- Fixed import order and object destructuring in FirstPersonEffects
- Improved global type definitions for bot, loadedData, and appViewer
- Added proper type definitions for Browser APIs and Canvas interfaces
- Resolved all TypeScript type checking issues
- Enhanced code quality with comprehensive type coverage
2025-06-29 19:23:17 +00:00

159 lines
5.6 KiB
TypeScript

import * as THREE from 'three'
import type { AtlasParser, TextureInfo } from 'mc-assets'
import { getLoadedImage } from 'mc-assets/dist/utils'
import { LoadedResourcesTransferrable, ResourcesManager } from '../../../src/resourcesManager'
import { WorldRendererThree } from './worldrendererThree'
export class FirstPersonEffects {
private readonly fireSprite: THREE.Sprite
private fireTextures: THREE.Texture[] = []
private currentTextureIndex = 0
private lastTextureUpdate = 0
private readonly TEXTURE_UPDATE_INTERVAL = 200 // 5 times per second
private readonly cameraGroup = new THREE.Group()
private readonly effectsGroup = new THREE.Group()
updateCameraGroup = true
constructor (private readonly worldRenderer: WorldRendererThree) {
this.worldRenderer.scene.add(this.cameraGroup)
this.cameraGroup.add(this.effectsGroup)
if (this.worldRenderer.resourcesManager.currentResources) {
void this.loadTextures()
}
this.worldRenderer.resourcesManager.on('assetsTexturesUpdated', () => {
void this.loadTextures()
})
// Create sprite
const spriteMaterial = new THREE.SpriteMaterial({
map: null,
transparent: true,
alphaTest: 0.1,
blending: THREE.AdditiveBlending, // Makes fire glow effect
depthTest: false, // Ensures fire always renders in front
depthWrite: false,
color: new THREE.Color(1, 0.8, 0.4), // Slightly warm tint
})
this.fireSprite = new THREE.Sprite(spriteMaterial)
this.fireSprite.visible = false
this.effectsGroup.add(this.fireSprite)
this.worldRenderer.onRender.push(() => {
this.update()
})
}
async loadTextures () {
const fireImageBase64 = [] as string[]
const resources = this.worldRenderer.resourcesManager.currentResources
if (!resources) {
console.warn('FirstPersonEffects: No resources available for loading fire textures')
return
}
// Cast resourcesManager to access blocksAtlasParser using proper types
const resourcesManager = this.worldRenderer.resourcesManager as ResourcesManager
const blocksAtlasParser = resourcesManager.blocksAtlasParser as AtlasParser
if (!blocksAtlasParser?.atlas?.latest) {
console.warn('FirstPersonEffects: Blocks atlas parser not available')
return
}
// Load all fire animation frames (fire_0, fire_1, etc.)
for (let i = 0; i < 32; i++) {
try {
const textureInfo = blocksAtlasParser.getTextureInfo(`fire_${i}`)
if (!textureInfo) break // Stop when no more frames available
const { atlas } = blocksAtlasParser
const defaultSize = atlas.latest.tileSize || 16
const { width: imageWidth = 256, height: imageHeight = 256 } = atlas.latest
const canvas = new OffscreenCanvas(
textureInfo.width ?? defaultSize,
textureInfo.height ?? defaultSize
)
const ctx = canvas.getContext('2d')
if (ctx && blocksAtlasParser.latestImage) {
const image = await getLoadedImage(blocksAtlasParser.latestImage)
const sourceX = textureInfo.u * imageWidth
const sourceY = textureInfo.v * imageHeight
const sourceWidth = textureInfo.width ?? defaultSize
const sourceHeight = textureInfo.height ?? defaultSize
ctx.drawImage(
image,
sourceX,
sourceY,
sourceWidth,
sourceHeight,
0,
0,
sourceWidth,
sourceHeight
)
const blob = await canvas.convertToBlob()
const url = URL.createObjectURL(blob)
fireImageBase64.push(url)
}
} catch (error) {
console.warn(`FirstPersonEffects: Error loading fire texture ${i}:`, error)
break
}
}
// Create textures from base64 images
this.fireTextures = fireImageBase64.map(base64 => {
const texture = new THREE.TextureLoader().load(base64)
texture.minFilter = THREE.NearestFilter
texture.magFilter = THREE.NearestFilter
return texture
})
console.log(`FirstPersonEffects: Loaded ${this.fireTextures.length} fire animation frames`)
}
setIsOnFire (isOnFire: boolean) {
this.fireSprite.visible = isOnFire
}
update () {
if (!this.fireSprite.visible || this.fireTextures.length === 0) return
const now = Date.now()
if (now - this.lastTextureUpdate >= this.TEXTURE_UPDATE_INTERVAL) {
this.currentTextureIndex = (this.currentTextureIndex + 1) % this.fireTextures.length
this.fireSprite.material.map = this.fireTextures[this.currentTextureIndex]
this.lastTextureUpdate = now
}
// Update camera group position and rotation
const { camera } = this.worldRenderer
if (this.updateCameraGroup && camera) {
this.cameraGroup.position.copy(camera.position)
this.cameraGroup.rotation.copy(camera.rotation)
}
// Position fire overlay in front of camera but fill the screen like in Minecraft
const distance = 0.1 // Very close to camera for overlay effect
this.effectsGroup.position.set(0, 0, -distance)
// Scale sprite to fill most of the screen like Minecraft's fire overlay
const { innerWidth, innerHeight } = window
const aspect = innerWidth / innerHeight
const { fov } = camera
const fovRadians = (fov * Math.PI) / 180
const height = 2 * Math.tan(fovRadians / 2) * distance
const width = height * aspect
// Make fire overlay larger to create immersive burning effect
this.fireSprite.scale.set(width * 1.8, height * 1.8, 1)
// Slightly offset the fire to the bottom of the screen like in Minecraft
this.fireSprite.position.set(0, -height * 0.3, 0)
}
}