forked from PrismarineJS/prismarine-web-client
-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: effects + system indicators in hud! (#95)
Co-authored-by: gguio <[email protected]>
- Loading branch information
Showing
12 changed files
with
413 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
.effectsScreen-container { | ||
position: fixed; | ||
top: 6%; | ||
left: 0px; | ||
z-index: -1; | ||
pointer-events: none; | ||
} | ||
|
||
.indicators-container { | ||
display: flex; | ||
font-size: 0.7em; | ||
} | ||
|
||
.effects-container { | ||
display: flex; | ||
flex-direction: column; | ||
} | ||
|
||
.effect-box { | ||
display: flex; | ||
align-items: center; | ||
} | ||
|
||
.effect-box__image { | ||
width: 23px; | ||
margin-right: 3px; | ||
} | ||
|
||
.effect-box__time { | ||
font-size: 0.65rem; | ||
} | ||
|
||
.effect-box__level { | ||
font-size: 0.45rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import 'iconify-icon' | ||
|
||
import type { Meta, StoryObj } from '@storybook/react' | ||
|
||
import IndicatorEffects, { defaultIndicatorsState } from './IndicatorEffects' | ||
import { images } from './effectsImages' | ||
|
||
const meta: Meta<typeof IndicatorEffects> = { | ||
component: IndicatorEffects | ||
} | ||
|
||
export default meta | ||
type Story = StoryObj<typeof IndicatorEffects>; | ||
|
||
export const Primary: Story = { | ||
args: { | ||
indicators: defaultIndicatorsState, | ||
effects: [ | ||
{ | ||
image: images.glowing, | ||
time: 200, | ||
level: 255, | ||
removeEffect (image: string) {}, | ||
reduceTime (image: string) {} | ||
}, | ||
{ | ||
image: images.absorption, | ||
time: 30, | ||
level: 99, | ||
removeEffect (image: string) {}, | ||
reduceTime (image: string) {} | ||
} | ||
], | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { useMemo, useEffect, useRef } from 'react' | ||
import PixelartIcon from './PixelartIcon' | ||
import './IndicatorEffects.css' | ||
|
||
|
||
|
||
function formatTime (seconds: number): string { | ||
if (seconds < 0) return '' | ||
const minutes = Math.floor(seconds / 60) | ||
const remainingSeconds = seconds % 60 | ||
const formattedMinutes = String(minutes).padStart(2, '0') | ||
const formattedSeconds = String(remainingSeconds).padStart(2, '0') | ||
return `${formattedMinutes}:${formattedSeconds}` | ||
} | ||
|
||
export type EffectType = { | ||
image: string, | ||
time: number, | ||
level: number, | ||
removeEffect: (image: string) => void, | ||
reduceTime: (image: string) => void | ||
} | ||
|
||
const EffectBox = ({ image, time, level }: Pick<EffectType, 'image' | 'time' | 'level'>) => { | ||
|
||
const formattedTime = useMemo(() => formatTime(time), [time]) | ||
|
||
return <div className='effect-box'> | ||
<img className='effect-box__image' src={image} alt='' /> | ||
<div> | ||
{formattedTime ? ( | ||
// if time is negative then effect is shown without time. | ||
// Component should be removed manually with time = 0 | ||
<div className='effect-box__time'>{formattedTime}</div> | ||
) : null } | ||
{level > 0 && level < 256 ? ( | ||
<div className='effect-box__level'>{level + 1}</div> | ||
) : null } | ||
</div> | ||
</div> | ||
} | ||
|
||
export const defaultIndicatorsState = { | ||
chunksLoading: false, | ||
readingFiles: false, | ||
readonlyFiles: false, | ||
writingFiles: false, // saving | ||
appHasErrors: false, | ||
} | ||
|
||
const indicatorIcons: Record<keyof typeof defaultIndicatorsState, string> = { | ||
chunksLoading: 'add-grid', | ||
readingFiles: 'arrow-bar-down', | ||
writingFiles: 'arrow-bar-up', | ||
appHasErrors: 'alert', | ||
readonlyFiles: 'file-off', | ||
} | ||
|
||
export default ({ indicators, effects }: {indicators: typeof defaultIndicatorsState, effects: readonly EffectType[]}) => { | ||
const effectsRef = useRef(effects) | ||
useEffect(() => { | ||
effectsRef.current = effects | ||
}, [effects]) | ||
|
||
useEffect(() => { | ||
// todo use more precise timer for each effect | ||
const interval = setInterval(() => { | ||
for (const [index, effect] of effectsRef.current.entries()) { | ||
if (effect.time === 0) { | ||
// effect.removeEffect(effect.image) | ||
return | ||
} | ||
effect.reduceTime(effect.image) | ||
} | ||
}, 1000) | ||
|
||
return () => { | ||
clearInterval(interval) | ||
} | ||
}, []) | ||
|
||
const indicatorsMapped = Object.entries(defaultIndicatorsState).map(([key, state]) => ({ | ||
icon: indicatorIcons[key], | ||
// preserve order | ||
state: indicators[key], | ||
})) | ||
return <div className='effectsScreen-container'> | ||
<div className='indicators-container'> | ||
{ | ||
indicatorsMapped.map((indicator) => <div key={indicator.icon} style={{ | ||
opacity: indicator.state ? 1 : 0, | ||
transition: 'opacity 0.1s', | ||
}}> | ||
<PixelartIcon iconName={indicator.icon} /> | ||
</div>) | ||
} | ||
</div> | ||
<div className='effects-container'> | ||
{ | ||
effects.map( | ||
(effect) => <EffectBox | ||
key={`effectBox-${effect.image}`} | ||
image={effect.image} | ||
time={effect.time} | ||
level={effect.level} | ||
/> | ||
) | ||
} | ||
</div> | ||
</div> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { proxy, useSnapshot } from 'valtio' | ||
import { useEffect, useMemo } from 'react' | ||
import { inGameError } from '../utils' | ||
import { fsState } from '../loadSave' | ||
import { miscUiState } from '../globalState' | ||
import IndicatorEffects, { EffectType, defaultIndicatorsState } from './IndicatorEffects' | ||
import { images } from './effectsImages' | ||
|
||
export const state = proxy({ | ||
indicators: { | ||
chunksLoading: false | ||
}, | ||
effects: [] as EffectType[] | ||
}) | ||
|
||
export const addEffect = (newEffect: Omit<EffectType, 'reduceTime' | 'removeEffect'>) => { | ||
const effectIndex = getEffectIndex(newEffect as EffectType) | ||
if (typeof effectIndex === 'number') { | ||
state.effects[effectIndex].time = newEffect.time | ||
state.effects[effectIndex].level = newEffect.level | ||
} else { | ||
const effect = { ...newEffect, reduceTime, removeEffect } | ||
state.effects.push(effect) | ||
} | ||
} | ||
|
||
const removeEffect = (image: string) => { | ||
for (const [index, effect] of (state.effects).entries()) { | ||
if (effect.image === image) { | ||
state.effects.splice(index, 1) | ||
} | ||
} | ||
} | ||
|
||
const reduceTime = (image: string) => { | ||
for (const [index, effect] of (state.effects).entries()) { | ||
if (effect.image === image) { | ||
effect.time -= 1 | ||
} | ||
} | ||
} | ||
|
||
const getEffectIndex = (newEffect: EffectType) => { | ||
for (const [index, effect] of (state.effects).entries()) { | ||
if (effect.image === newEffect.image) { | ||
return index | ||
} | ||
} | ||
return null | ||
} | ||
|
||
export default () => { | ||
const stateIndicators = useSnapshot(state.indicators) | ||
const { hasErrors } = useSnapshot(miscUiState) | ||
const { isReadonly, openReadOperations, openWriteOperations } = useSnapshot(fsState) | ||
const allIndicators: typeof defaultIndicatorsState = { | ||
readonlyFiles: isReadonly, | ||
writingFiles: openWriteOperations > 0, | ||
readingFiles: openReadOperations > 0, | ||
appHasErrors: hasErrors, | ||
...stateIndicators, | ||
} | ||
|
||
useEffect(() => { | ||
let alreadyWaiting = false | ||
const listener = () => { | ||
if (alreadyWaiting) return | ||
state.indicators.chunksLoading = true | ||
alreadyWaiting = true | ||
void viewer.waitForChunksToRender().then(() => { | ||
state.indicators.chunksLoading = false | ||
alreadyWaiting = false | ||
}) | ||
} | ||
viewer.world.renderUpdateEmitter.on('dirty', listener) | ||
|
||
return () => { | ||
viewer.world.renderUpdateEmitter.off('dirty', listener) | ||
} | ||
}, []) | ||
|
||
const effects = useSnapshot(state.effects) | ||
|
||
useMemo(() => { | ||
const effectsImages = Object.fromEntries(loadedData.effectsArray.map((effect) => { | ||
const nameKebab = effect.name.replaceAll(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).slice(1) | ||
return [effect.id, images[nameKebab]] | ||
})) | ||
bot.on('entityEffect', (entity, effect) => { | ||
if (entity.id !== bot.entity.id) return | ||
const image = effectsImages[effect.id] ?? null | ||
if (!image) { | ||
inGameError(`received unknown effect id ${effect.id}}`) | ||
return | ||
} | ||
const newEffect = { | ||
image, | ||
time: effect.duration / 20, // duration received in ticks | ||
level: effect.amplifier, | ||
} | ||
addEffect(newEffect) | ||
}) | ||
bot.on('entityEffectEnd', (entity, effect) => { | ||
if (entity.id !== bot.entity.id) return | ||
const image = effectsImages[effect.id] ?? null | ||
if (!image) { | ||
inGameError(`received unknown effect id ${effect.id}}}`) | ||
return | ||
} | ||
removeEffect(image) | ||
}) | ||
}, []) | ||
|
||
return <IndicatorEffects | ||
indicators={allIndicators} | ||
effects={effects} | ||
/> | ||
} |
Oops, something went wrong.