Skip to content

Commit caf4695

Browse files
committed
feat: silly player on fire renderer effect
1 parent 167b49d commit caf4695

File tree

4 files changed

+218
-0
lines changed

4 files changed

+218
-0
lines changed

renderer/viewer/lib/basePlayerState.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const getInitialPlayerState = () => proxy({
4848
heldItemMain: undefined as HandItemBlock | undefined,
4949
heldItemOff: undefined as HandItemBlock | undefined,
5050
perspective: 'first_person' as CameraPerspective,
51+
onFire: false,
5152

5253
cameraSpectatingEntity: undefined as number | undefined,
5354

src/entities.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,28 @@ customEvents.on('gameLoaded', () => {
126126
if (entityStatus === EntityStatus.HURT) {
127127
getThreeJsRendererMethods()?.damageEntity(entityId, entityStatus)
128128
}
129+
130+
if (entityStatus === EntityStatus.BURNED) {
131+
updateEntityStates(entityId, true, true)
132+
}
133+
})
134+
135+
// on fire events
136+
bot._client.on('entity_metadata', (data) => {
137+
if (data.entityId !== bot.entity.id) return
138+
handleEntityMetadata(data)
139+
})
140+
141+
bot.on('end', () => {
142+
if (onFireTimeout) {
143+
clearTimeout(onFireTimeout)
144+
}
145+
})
146+
147+
bot.on('respawn', () => {
148+
if (onFireTimeout) {
149+
clearTimeout(onFireTimeout)
150+
}
129151
})
130152

131153
const updateCamera = (entity: Entity) => {
@@ -296,3 +318,44 @@ customEvents.on('gameLoaded', () => {
296318
})
297319

298320
})
321+
322+
// Constants
323+
const SHARED_FLAGS_KEY = 0
324+
const ENTITY_FLAGS = {
325+
ON_FIRE: 0x01, // Bit 0
326+
SNEAKING: 0x02, // Bit 1
327+
SPRINTING: 0x08, // Bit 3
328+
SWIMMING: 0x10, // Bit 4
329+
INVISIBLE: 0x20, // Bit 5
330+
GLOWING: 0x40, // Bit 6
331+
FALL_FLYING: 0x80 // Bit 7 (elytra flying)
332+
}
333+
334+
let onFireTimeout: NodeJS.Timeout | undefined
335+
const updateEntityStates = (entityId: number, onFire: boolean, timeout?: boolean) => {
336+
if (entityId !== bot.entity.id) return
337+
appViewer.playerState.reactive.onFire = onFire
338+
if (onFireTimeout) {
339+
clearTimeout(onFireTimeout)
340+
}
341+
if (timeout) {
342+
onFireTimeout = setTimeout(() => {
343+
updateEntityStates(entityId, false, false)
344+
}, 5000)
345+
}
346+
}
347+
348+
// Process entity metadata packet
349+
function handleEntityMetadata (packet: { entityId: number, metadata: Array<{ key: number, type: string, value: number }> }) {
350+
const { entityId, metadata } = packet
351+
352+
// Find shared flags in metadata
353+
const flagsData = metadata.find(meta => meta.key === SHARED_FLAGS_KEY &&
354+
meta.type === 'byte')
355+
356+
// Update fire state if flags were found
357+
if (flagsData) {
358+
const wasOnFire = appViewer.playerState.reactive.onFire
359+
appViewer.playerState.reactive.onFire = (flagsData.value & ENTITY_FLAGS.ON_FIRE) !== 0
360+
}
361+
}

src/react/FireRenderer.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/* eslint-disable no-await-in-loop */
2+
import { useSnapshot } from 'valtio'
3+
import { useEffect, useState } from 'react'
4+
import { getLoadedImage } from 'mc-assets/dist/utils'
5+
import { createCanvas } from 'renderer/viewer/lib/utils'
6+
7+
const TEXTURE_UPDATE_INTERVAL = 100 // 5 times per second
8+
9+
export default () => {
10+
const { onFire, perspective } = useSnapshot(appViewer.playerState.reactive)
11+
const [fireTextures, setFireTextures] = useState<string[]>([])
12+
const [currentTextureIndex, setCurrentTextureIndex] = useState(0)
13+
14+
useEffect(() => {
15+
let animationFrameId: number
16+
let lastTextureUpdate = 0
17+
18+
const updateTexture = (timestamp: number) => {
19+
if (onFire && fireTextures.length > 0) {
20+
if (timestamp - lastTextureUpdate >= TEXTURE_UPDATE_INTERVAL) {
21+
setCurrentTextureIndex(prev => (prev + 1) % fireTextures.length)
22+
lastTextureUpdate = timestamp
23+
}
24+
}
25+
animationFrameId = requestAnimationFrame(updateTexture)
26+
}
27+
28+
animationFrameId = requestAnimationFrame(updateTexture)
29+
return () => cancelAnimationFrame(animationFrameId)
30+
}, [onFire, fireTextures])
31+
32+
useEffect(() => {
33+
const loadTextures = async () => {
34+
const fireImageUrls: string[] = []
35+
36+
const { resourcesManager } = appViewer
37+
const { blocksAtlasParser } = resourcesManager
38+
if (!blocksAtlasParser?.atlas?.latest) {
39+
console.warn('FireRenderer: Blocks atlas parser not available')
40+
return
41+
}
42+
43+
const keys = Object.keys(blocksAtlasParser.atlas.latest.textures).filter(key => /^fire_\d+$/.exec(key))
44+
for (const key of keys) {
45+
const textureInfo = blocksAtlasParser.getTextureInfo(key) as { u: number, v: number, width?: number, height?: number }
46+
if (textureInfo) {
47+
const defaultSize = blocksAtlasParser.atlas.latest.tileSize
48+
const imageWidth = blocksAtlasParser.atlas.latest.width
49+
const imageHeight = blocksAtlasParser.atlas.latest.height
50+
const textureWidth = textureInfo.width ?? defaultSize
51+
const textureHeight = textureInfo.height ?? defaultSize
52+
53+
// Create a temporary canvas for the full texture
54+
const tempCanvas = createCanvas(textureWidth, textureHeight)
55+
const tempCtx = tempCanvas.getContext('2d')
56+
if (tempCtx && blocksAtlasParser.latestImage) {
57+
const image = await getLoadedImage(blocksAtlasParser.latestImage)
58+
tempCtx.drawImage(
59+
image,
60+
textureInfo.u * imageWidth,
61+
textureInfo.v * imageHeight,
62+
textureWidth,
63+
textureHeight,
64+
0,
65+
0,
66+
textureWidth,
67+
textureHeight
68+
)
69+
70+
// Create final canvas with only top 20% of the texture
71+
const finalHeight = Math.ceil(textureHeight * 0.4)
72+
const canvas = createCanvas(textureWidth, finalHeight)
73+
const ctx = canvas.getContext('2d')
74+
if (ctx) {
75+
// Draw only the top portion
76+
ctx.drawImage(
77+
tempCanvas,
78+
0,
79+
0, // Start from top
80+
textureWidth,
81+
finalHeight,
82+
0,
83+
0,
84+
textureWidth,
85+
finalHeight
86+
)
87+
88+
const blob = await canvas.convertToBlob()
89+
const url = URL.createObjectURL(blob)
90+
fireImageUrls.push(url)
91+
}
92+
}
93+
}
94+
}
95+
96+
setFireTextures(fireImageUrls)
97+
}
98+
99+
// Load textures initially
100+
if (appViewer.resourcesManager.currentResources) {
101+
void loadTextures()
102+
}
103+
104+
// Set up listener for texture updates
105+
const onAssetsUpdated = () => {
106+
void loadTextures()
107+
}
108+
appViewer.resourcesManager.on('assetsTexturesUpdated', onAssetsUpdated)
109+
110+
// Cleanup
111+
return () => {
112+
appViewer.resourcesManager.off('assetsTexturesUpdated', onAssetsUpdated)
113+
// Cleanup texture URLs
114+
for (const url of fireTextures) URL.revokeObjectURL(url)
115+
}
116+
}, [])
117+
118+
if (!onFire || fireTextures.length === 0 || perspective !== 'first_person') return null
119+
120+
return (
121+
<div
122+
className='fire-renderer-container'
123+
style={{
124+
position: 'fixed',
125+
left: 0,
126+
right: 0,
127+
bottom: 0,
128+
height: '20dvh',
129+
pointerEvents: 'none',
130+
display: 'flex',
131+
justifyContent: 'center',
132+
alignItems: 'flex-end',
133+
overflow: 'hidden'
134+
}}
135+
>
136+
<div
137+
style={{
138+
position: 'absolute',
139+
width: '100%',
140+
height: '100%',
141+
backgroundImage: `url(${fireTextures[currentTextureIndex]})`,
142+
backgroundSize: '50% 100%',
143+
backgroundPosition: 'center',
144+
backgroundRepeat: 'repeat-x',
145+
opacity: 0.7,
146+
filter: 'brightness(1.2) contrast(1.2)',
147+
mixBlendMode: 'screen'
148+
}}
149+
/>
150+
</div>
151+
)
152+
}

src/reactUi.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import CreditsAboutModal from './react/CreditsAboutModal'
6666
import GlobalOverlayHints from './react/GlobalOverlayHints'
6767
import FullscreenTime from './react/FullscreenTime'
6868
import StorageConflictModal from './react/StorageConflictModal'
69+
import FireRenderer from './react/FireRenderer'
6970

7071
const isFirefox = ua.getBrowser().name === 'Firefox'
7172
if (isFirefox) {
@@ -171,6 +172,7 @@ const InGameUi = () => {
171172
<VoiceMicrophone />
172173
<ChunksDebugScreen />
173174
<RendererDebugMenu />
175+
{!disabledUiParts.includes('fire') && <FireRenderer />}
174176
</PerComponentErrorBoundary>
175177
</div>
176178

0 commit comments

Comments
 (0)