diff --git a/renderer/viewer/lib/basePlayerState.ts b/renderer/viewer/lib/basePlayerState.ts index f9e4b2c9..6aea6159 100644 --- a/renderer/viewer/lib/basePlayerState.ts +++ b/renderer/viewer/lib/basePlayerState.ts @@ -48,6 +48,7 @@ export const getInitialPlayerState = () => proxy({ heldItemMain: undefined as HandItemBlock | undefined, heldItemOff: undefined as HandItemBlock | undefined, perspective: 'first_person' as CameraPerspective, + onFire: false, cameraSpectatingEntity: undefined as number | undefined, }) diff --git a/renderer/viewer/three/firstPersonEffects.ts b/renderer/viewer/three/firstPersonEffects.ts new file mode 100644 index 00000000..86d4169d --- /dev/null +++ b/renderer/viewer/three/firstPersonEffects.ts @@ -0,0 +1,159 @@ +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) + } +} diff --git a/renderer/viewer/three/worldrendererThree.ts b/renderer/viewer/three/worldrendererThree.ts index c4482052..24f33f23 100644 --- a/renderer/viewer/three/worldrendererThree.ts +++ b/renderer/viewer/three/worldrendererThree.ts @@ -22,6 +22,7 @@ import { getItemUv } from './appShared' import { Entities } from './entities' import { ThreeJsSound } from './threeJsSound' import { CameraShake } from './cameraShake' +import { FirstPersonEffects } from './firstPersonEffects' import { ThreeJsMedia } from './threeJsMedia' import { Fountain } from './threeJsParticles' @@ -49,6 +50,7 @@ export class WorldRendererThree extends WorldRendererCommon { cameraContainer: THREE.Object3D media: ThreeJsMedia waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] } + firstPersonEffects: FirstPersonEffects camera: THREE.PerspectiveCamera renderTimeAvg = 0 sectionsOffsetsAnimations = {} as { @@ -99,6 +101,7 @@ export class WorldRendererThree extends WorldRendererCommon { this.soundSystem = new ThreeJsSound(this) this.cameraShake = new CameraShake(this, this.onRender) + this.firstPersonEffects = new FirstPersonEffects(this) this.media = new ThreeJsMedia(this) // this.fountain = new Fountain(this.scene, this.scene, { // position: new THREE.Vector3(0, 10, 0), @@ -184,6 +187,10 @@ export class WorldRendererThree extends WorldRendererCommon { this.updateCamera(vecPos, this.cameraShake.getBaseRotation().yaw, this.cameraShake.getBaseRotation().pitch) // todo also update camera when block within camera was changed }) + this.onReactivePlayerStateUpdated('onFire', (value) => { + // Update fire effect when player fire status changes + this.firstPersonEffects.setIsOnFire(value) + }) } override watchReactiveConfig () { diff --git a/src/mineflayer/playerState.ts b/src/mineflayer/playerState.ts index b7b4d2bd..fcd0d77d 100644 --- a/src/mineflayer/playerState.ts +++ b/src/mineflayer/playerState.ts @@ -81,6 +81,8 @@ export class PlayerStateControllerMain { bot.on('physicsTick', () => { if (this.isUsingItem) this.reactive.itemUsageTicks++ updateSneakingOrFlying() + // Update fire status + this.updateFireStatus() }) // todo move from gameAdditionalState to reactive directly subscribeKey(gameAdditionalState, 'isSneaking', () => { @@ -188,6 +190,53 @@ export class PlayerStateControllerMain { } // #endregion + + // #region Fire Status + private updateFireStatus () { + if (!bot?.entity || this.disableStateUpdates) return + + // Check if player is on fire by looking for burning-related effects and entity metadata + let isOnFire = false + + // Method 1: Check entity metadata/properties for fire status + try { + // These are the most common ways fire status is tracked in Minecraft + isOnFire = (bot.entity as any).onFire || + (bot.entity as any).fireTicks > 0 || + (bot.entity as any).fire > 0 || + false + } catch { + // Fallback if properties don't exist + } + + // Method 2: Check for fire-related damage effects (when fire resistance is not active) + if (!isOnFire) { + const hasFireResistance = Object.values(bot.entity.effects ?? {}).some((effect: any) => loadedData.effects?.[effect.id]?.name === 'fire_resistance') + + // If no fire resistance and recently took damage, might be on fire + // This is a heuristic approach since we don't have direct fire status + if (!hasFireResistance) { + // Could add more sophisticated fire detection here based on damage patterns + } + } + + // Debug mode: Allow manually triggering fire effect for testing + // You can test the fire effect by running: window.playerState.setOnFire(true) + if ((window as any).debugFireEffect !== undefined) { + isOnFire = (window as any).debugFireEffect + } + + if (this.reactive.onFire !== isOnFire) { + this.reactive.onFire = isOnFire + } + } + + // Debug method to manually set fire status for testing + setOnFire (value: boolean) { + (window as any).debugFireEffect = value + } + + // #endregion } export const playerState = new PlayerStateControllerMain() diff --git a/src/react/StorageConflictModal.tsx b/src/react/StorageConflictModal.tsx index 966292d5..e1d3299d 100644 --- a/src/react/StorageConflictModal.tsx +++ b/src/react/StorageConflictModal.tsx @@ -17,7 +17,7 @@ export default () => { if (!isModalActive/* || conflicts.length === 0 */) return null const clampText = (text: string) => { - if (typeof text !== "string") text = JSON.stringify(text) + if (typeof text !== 'string') text = JSON.stringify(text) return text.length > 30 ? text.slice(0, 30) + '...' : text } diff --git a/src/types/modules.d.ts b/src/types/modules.d.ts new file mode 100644 index 00000000..a58c699d --- /dev/null +++ b/src/types/modules.d.ts @@ -0,0 +1,544 @@ +/** + * Enhanced module declarations for better type safety in the Minecraft web client + * Provides type definitions for external modules and enhances existing ones + */ + +// Enhanced THREE.js type augmentations +declare module 'three' { + interface Material { + map?: Texture | null + } + + interface SpriteMaterial extends Material { + map?: Texture | null + transparent?: boolean + alphaTest?: number + blending?: Blending + depthTest?: boolean + depthWrite?: boolean + color?: Color + } +} + +// Vec3 module declarations +declare module 'vec3' { + export class Vec3 { + constructor (x?: number, y?: number, z?: number) + x: number + y: number + z: number + + set (x: number, y: number, z: number): this + add (other: Vec3): Vec3 + subtract (other: Vec3): Vec3 + multiply (scalar: number): Vec3 + divide (scalar: number): Vec3 + dot (other: Vec3): number + cross (other: Vec3): Vec3 + length (): number + normalize (): Vec3 + distance (other: Vec3): number + equals (other: Vec3): boolean + clone (): Vec3 + offset (dx: number, dy: number, dz: number): Vec3 + plus (other: Vec3): Vec3 + minus (other: Vec3): Vec3 + scaled (scalar: number): Vec3 + abs (): Vec3 + floor (): Vec3 + ceil (): Vec3 + round (): Vec3 + translate (dx: number, dy: number, dz: number): Vec3 + toString (): string + toArray (): [number, number, number] + + static fromArray (arr: [number, number, number]): Vec3 + } + export default Vec3 +} + +// Prismarine-nbt module declarations +declare module 'prismarine-nbt' { + export interface NBTData { + name: string + value: any + type: string + } + + export interface ParsedNBT { + parsed: NBTData + type: string + metadata: any + } + + export function parse (buffer: Buffer, littleEndian?: boolean): Promise + export function parseUncompressed (buffer: Buffer, littleEndian?: boolean): ParsedNBT + export function writeUncompressed (value: any, littleEndian?: boolean): Buffer + export function simplify (data: any): any + export function serialize (nbt: any): Buffer + + export class Writer { + constructor (littleEndian?: boolean) + writeTag (tag: any): void + getBuffer (): Buffer + } + + export class Reader { + constructor (buffer: Buffer, littleEndian?: boolean) + readTag (): any + } + + export default { + parse, + parseUncompressed, + writeUncompressed, + simplify, + serialize, + Writer, + Reader + } +} + +// @tweenjs/tween.js module declarations +declare module '@tweenjs/tween.js' { + export interface TweenEasing { + Linear: { + None (k: number): number + } + Quadratic: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Cubic: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Quartic: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Quintic: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Sinusoidal: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Exponential: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Circular: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Elastic: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Back: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + Bounce: { + In (k: number): number + Out (k: number): number + InOut (k: number): number + } + } + + export class Tween> { + constructor (object: T, group?: Group) + to (properties: Partial, duration: number): this + start (time?: number): this + stop (): this + end (): this + stopChainedTweens (): this + group (group: Group): this + delay (amount: number): this + repeat (times: number): this + repeatDelay (amount: number): this + yoyo (yoyo: boolean): this + easing (easingFunction: (k: number) => number): this + interpolation (interpolationFunction: (v: number[], k: number) => number): this + chain (...tweens: Tween[]): this + onStart (callback: (object: T) => void): this + onUpdate (callback: (object: T, elapsed: number) => void): this + onRepeat (callback: (object: T) => void): this + onComplete (callback: (object: T) => void): this + onStop (callback: (object: T) => void): this + update (time: number): boolean + isPlaying (): boolean + isPaused (): boolean + pause (time?: number): this + resume (time?: number): this + duration (duration?: number): number + getDuration (): number + getId (): number + } + + export class Group { + constructor () + getAll (): Tween[] + removeAll (): void + add (tween: Tween): void + remove (tween: Tween): void + update (time?: number): boolean + } + + export const Easing: TweenEasing + + export function update (time?: number): boolean + export function getAll (): Tween[] + export function removeAll (): void + export function add (tween: Tween): void + export function remove (tween: Tween): void + export function now (): number + + export default { + Tween, + Group, + Easing, + update, + getAll, + removeAll, + add, + remove, + now + } +} + +// mc-assets module declarations with enhanced types +declare module 'mc-assets' { + export interface AtlasParser { + atlas: { + latest: { + tileSize: number + width: number + height: number + textures: Record + suSv: number + } + } + latestImage?: string + getTextureInfo(name: string): TextureInfo | null | undefined + createDebugImage(includeText?: boolean): Promise + makeNewAtlas(...args: any[]): Promise<{ atlas: any, canvas: HTMLCanvasElement }> + } + + export interface TextureInfo { + u: number + v: number + width?: number + height?: number + su?: number + sv?: number + } + + export interface BlockModel { + elements?: any[] + textures?: Record + display?: Record + gui_light?: string + tints?: any + [key: string]: any + } + + export interface ItemsAtlasesOutputJson { + tileSize: number + width: number + height: number + textures: Record + suSv: number + } +} + +declare module 'mc-assets/dist/utils' { + export function getLoadedImage(src: string | HTMLImageElement): Promise + export function versionToNumber(version: string): number +} + +declare module 'mc-assets/dist/atlasParser' { + export { AtlasParser, ItemsAtlasesOutputJson } from 'mc-assets' +} + +declare module 'mc-assets/dist/worldBlockProvider' { + export interface WorldBlockProvider { + getBlockModel(name: string): any + getTextureUV(textureName: string): number[] | undefined + } + + export default function worldBlockProvider( + blockstatesModels: any, + atlas: any, + version: string + ): WorldBlockProvider +} + +declare module 'mc-assets/dist/itemsRenderer' { + export interface ItemsRendererConstructor { + new ( + version: string, + blockstatesModels: any, + itemsAtlasParser: any, + blocksAtlasParser: any + ): any + } + export const ItemsRenderer: ItemsRendererConstructor +} + +declare module 'mc-assets/dist/itemDefinitions' { + export interface ItemSelector { + properties?: { + 'minecraft:using_item'?: boolean + 'minecraft:use_duration'?: number + 'minecraft:use_cycle'?: number + 'minecraft:display_context'?: string + } + } + + export function getItemDefinition(store: any, selector: any): any + export function getLoadedItemDefinitionsStore(data: any): any +} + +// Enhanced valtio type declarations +declare module 'valtio' { + export function proxy(initialObject: T): T + export function subscribe(proxy: T, callback: (ops: any[]) => void): () => void + export function ref(obj: T): T + export function useSnapshot(proxy: T): Readonly +} + +declare module 'valtio/utils' { + export function subscribeKey( + proxy: T, + key: K, + callback: (value: T[K]) => void + ): () => void +} + +declare module 'valtio/vanilla' { + export function proxy(initialObject: T): T + export function subscribe(proxy: T, callback: (ops: any[]) => void): () => void + export function snapshot(proxy: T): Readonly +} + +// Three.js addon modules +declare module 'three/examples/jsm/controls/OrbitControls.js' { + import { Camera, EventDispatcher } from 'three' + + export interface OrbitControlsConstructor { + new (object: Camera, domElement?: HTMLElement): OrbitControlsInstance + } + + export interface OrbitControlsInstance extends EventDispatcher { + enabled: boolean + enableDamping: boolean + dampingFactor: number + update(): boolean + dispose(): void + } + + export const OrbitControls: OrbitControlsConstructor +} + +declare module 'three/examples/jsm/webxr/VRButton.js' { + import { WebGLRenderer } from 'three' + + export const VRButton: { + createButton(renderer: WebGLRenderer): HTMLElement + } +} + +declare module 'three/addons/controls/OrbitControls.js' { + export { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' +} + +declare module 'three-stdlib' { + import { Material, Object3D, BufferGeometry, Vector2 } from 'three' + + export class LineMaterial extends Material { + constructor (parameters?: any) + color: any + linewidth: number + resolution: Vector2 + dashOffset: number + } + + export class LineSegmentsGeometry extends BufferGeometry { + constructor () + setPositions (positions: number[]): this + fromEdgesGeometry (geometry: BufferGeometry): this + } + + export class Wireframe extends Object3D { + constructor (geometry?: BufferGeometry, material?: Material) + computeLineDistances (): void + } + + export class OBJLoader { + constructor () + load (url: string, onLoad?: (object: any) => void): void + parse (data: string): Object3D + } +} + +// Additional commonly used modules +declare module 'stats.js' { + export default class Stats { + constructor () + dom: HTMLDivElement + begin (): void + end (): void + update (): void + setMode (mode: number): void + showPanel (panel: number): void + } +} + +declare module 'debug' { + interface Debug { + (namespace: string): Debugger + enabled (namespaces: string): boolean + humanize (val: number): string + names: RegExp[] + skips: RegExp[] + formatters: Record string> + } + + interface Debugger { + (formatter: any, ...args: any[]): void + enabled: boolean + log: (...args: any[]) => any + namespace: string + destroy (): boolean + extend (namespace: string, delimiter?: string): Debugger + } + + const debug: Debug + export = debug +} + +// Enhanced events module with missing methods +declare module 'events' { + export class EventEmitter { + static defaultMaxListeners: number + + constructor () + on (event: string, listener: (...args: any[]) => void): this + once (event: string, listener: (...args: any[]) => void): this + emit (event: string, ...args: any[]): boolean + off (event: string, listener: (...args: any[]) => void): this + removeListener (event: string, listener: (...args: any[]) => void): this + removeAllListeners (event?: string): this + listeners (event: string): Function[] + listenerCount (event: string): number + addListener (event: string, listener: (...args: any[]) => void): this + prependListener (event: string, listener: (...args: any[]) => void): this + prependOnceListener (event: string, listener: (...args: any[]) => void): this + setMaxListeners (n: number): this + getMaxListeners (): number + eventNames (): Array + rawListeners (event: string): Function[] + } +} + +// Browser API enhancements +interface Window { + playerState?: { + setOnFire(value: boolean): void + reactive: { + onFire: boolean + } + } + debugFireEffect?: boolean +} + +// Enhanced canvas and WebGL types +interface OffscreenCanvas { + convertToBlob(options?: { type?: string, quality?: number }): Promise + getContext(contextId: '2d'): OffscreenCanvasRenderingContext2D | null + getContext(contextId: 'webgl' | 'webgl2'): WebGLRenderingContext | null +} + +interface OffscreenCanvasRenderingContext2D { + drawImage( + image: HTMLImageElement | ImageBitmap, + sx: number, sy: number, sw: number, sh: number, + dx: number, dy: number, dw: number, dh: number + ): void + drawImage(image: HTMLImageElement | ImageBitmap, dx: number, dy: number): void +} + +// Bot entity type enhancements for fire detection +interface BotEntity { + onFire?: boolean + fireTicks?: number + fire?: number + effects?: Record + position: { x: number, y: number, z: number } + yaw: number + pitch: number + onGround: boolean + velocity: { x: number, y: number, z: number } +} + +// Enhanced bot client interface +interface BotClient { + on (event: string, callback: (...args: any[]) => void): void + prependListener (event: string, callback: (...args: any[]) => void): void + write (name: T, data: any): Buffer +} + +// Global bot interface enhancement +declare global { + const bot: { + entity?: BotEntity + _client?: BotClient + game?: { + gameMode?: string + dimension?: string + } + username?: string + inventory?: { + slots: any[] + } + heldItem?: any + controlState?: { + sneak: boolean + } + on(event: string, callback: (...args: any[]) => void): void + } + + const loadedData: { + effects?: Record + blocksByName?: Record + items?: Record + blocksByStateId?: Record + } + + const customEvents: { + on(event: string, callback: (...args: any[]) => void): void + } + + const appViewer: { + backend?: { + updateCamera(position: any, yaw: number, pitch: number): void + } + resourcesManager: any + } + + const PrismarineBlock: any +} + +export {} \ No newline at end of file