@@ -177,9 +180,8 @@ export default () => {
XYZ: {pos.x.toFixed(3)} / {pos.y.toFixed(3)} / {pos.z.toFixed(3)}
Chunk: {Math.floor(pos.x % 16)} ~ {Math.floor(pos.z % 16)} in {Math.floor(pos.x / 16)} ~ {Math.floor(pos.z / 16)}
-
Section: {Math.floor(pos.x / 16) * 16}, {Math.floor(pos.y / 16) * 16}, {Math.floor(pos.z / 16) * 16}
Packets: {packetsString}
-
Client TPS: {clientTps} {serverTps ? `Server TPS: ${serverTps.value} ${serverTps.frozen ? '(frozen)' : ''}` : ''}
+
Client TPS: {clientTps}
Facing (viewer): {bot.entity.yaw.toFixed(3)} {bot.entity.pitch.toFixed(3)}
Facing (minecraft): {quadsDescription[minecraftQuad.current]} ({minecraftYaw.current.toFixed(1)} {(bot.entity.pitch * -180 / Math.PI).toFixed(1)})
Light: {blockL} ({skyL} sky)
@@ -264,19 +266,14 @@ const hardcodedListOfDebugPacketsToIgnore = {
'playerlist_header',
'scoreboard_objective',
'scoreboard_score',
- 'entity_status',
- 'set_ticking_state',
- 'ping_response',
- 'block_change',
- 'damage_event'
+ 'entity_status'
],
sent: [
'pong',
'position',
'look',
'keep_alive',
- 'position_look',
- 'ping_request'
+ 'position_look'
]
}
diff --git a/src/react/DiveTransition.tsx b/src/react/DiveTransition.tsx
index 754ed8b2..82837876 100644
--- a/src/react/DiveTransition.tsx
+++ b/src/react/DiveTransition.tsx
@@ -1,11 +1,25 @@
import { useEffect, useState } from 'react'
-import { motion, AnimatePresence } from 'framer-motion'
+import { Transition } from 'react-transition-group'
import styles from './DiveTransition.module.css'
-const durationInSeconds = 0.3
-const durationInMs = durationInSeconds * 1000
+// dive animation from framework7
-export default ({ children, open, isError = false }) => {
+const startStyle = { opacity: 0, transform: 'translateZ(-150px)' }
+const endExitStyle = { opacity: 0, transform: 'translateZ(150px)' }
+const endStyle = { opacity: 1, transform: 'translateZ(0)' }
+
+const stateStyles = {
+ entering: endStyle,
+ entered: endStyle,
+ exiting: endExitStyle,
+ exited: endExitStyle,
+}
+const duration = 300
+const basicStyle = {
+ transition: `${duration}ms ease-in-out all`,
+}
+
+export default ({ children, open }) => {
const [mounted, setMounted] = useState(false)
useEffect(() => {
@@ -16,7 +30,7 @@ export default ({ children, open, isError = false }) => {
if (mounted && !open) {
timeout = setTimeout(() => {
setMounted(false)
- }, durationInMs)
+ }, duration)
}
return () => {
if (timeout) clearTimeout(timeout)
@@ -25,20 +39,17 @@ export default ({ children, open, isError = false }) => {
if (!mounted) return null
- return (
-
- {open && (
-
-
- {children}
-
-
- )}
-
- )
+ return
+ {(state) => {
+ return
+ {/* todo resolve compl */}
+
{children}
+
+ }}
+
}
diff --git a/src/react/FireRenderer.tsx b/src/react/FireRenderer.tsx
deleted file mode 100644
index 20ad4606..00000000
--- a/src/react/FireRenderer.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-/* eslint-disable no-await-in-loop */
-import { useSnapshot } from 'valtio'
-import { useEffect, useState } from 'react'
-import { getLoadedImage } from 'mc-assets/dist/utils'
-import { createCanvas } from 'renderer/viewer/lib/utils'
-
-const TEXTURE_UPDATE_INTERVAL = 100 // 5 times per second
-
-export default () => {
- const { onFire, perspective } = useSnapshot(appViewer.playerState.reactive)
- const [fireTextures, setFireTextures] = useState
([])
- const [currentTextureIndex, setCurrentTextureIndex] = useState(0)
-
- useEffect(() => {
- let animationFrameId: number
- let lastTextureUpdate = 0
-
- const updateTexture = (timestamp: number) => {
- if (onFire && fireTextures.length > 0) {
- if (timestamp - lastTextureUpdate >= TEXTURE_UPDATE_INTERVAL) {
- setCurrentTextureIndex(prev => (prev + 1) % fireTextures.length)
- lastTextureUpdate = timestamp
- }
- }
- animationFrameId = requestAnimationFrame(updateTexture)
- }
-
- animationFrameId = requestAnimationFrame(updateTexture)
- return () => cancelAnimationFrame(animationFrameId)
- }, [onFire, fireTextures])
-
- useEffect(() => {
- const loadTextures = async () => {
- const fireImageUrls: string[] = []
-
- const { resourcesManager } = appViewer
- const { blocksAtlasParser } = resourcesManager
- if (!blocksAtlasParser?.atlas?.latest) {
- console.warn('FireRenderer: Blocks atlas parser not available')
- return
- }
-
- const keys = Object.keys(blocksAtlasParser.atlas.latest.textures).filter(key => /^fire_\d+$/.exec(key))
- for (const key of keys) {
- const textureInfo = blocksAtlasParser.getTextureInfo(key) as { u: number, v: number, width?: number, height?: number }
- if (textureInfo) {
- const defaultSize = blocksAtlasParser.atlas.latest.tileSize
- const imageWidth = blocksAtlasParser.atlas.latest.width
- const imageHeight = blocksAtlasParser.atlas.latest.height
- const textureWidth = textureInfo.width ?? defaultSize
- const textureHeight = textureInfo.height ?? defaultSize
-
- // Create a temporary canvas for the full texture
- const tempCanvas = createCanvas(textureWidth, textureHeight)
- const tempCtx = tempCanvas.getContext('2d')
- if (tempCtx && blocksAtlasParser.latestImage) {
- const image = await getLoadedImage(blocksAtlasParser.latestImage)
- tempCtx.drawImage(
- image,
- textureInfo.u * imageWidth,
- textureInfo.v * imageHeight,
- textureWidth,
- textureHeight,
- 0,
- 0,
- textureWidth,
- textureHeight
- )
-
- // Create final canvas with only top 20% of the texture
- const finalHeight = Math.ceil(textureHeight * 0.4)
- const canvas = createCanvas(textureWidth, finalHeight)
- const ctx = canvas.getContext('2d')
- if (ctx) {
- // Draw only the top portion
- ctx.drawImage(
- tempCanvas,
- 0,
- 0, // Start from top
- textureWidth,
- finalHeight,
- 0,
- 0,
- textureWidth,
- finalHeight
- )
-
- const blob = await canvas.convertToBlob()
- const url = URL.createObjectURL(blob)
- fireImageUrls.push(url)
- }
- }
- }
- }
-
- setFireTextures(fireImageUrls)
- }
-
- // Load textures initially
- if (appViewer.resourcesManager.currentResources) {
- void loadTextures()
- }
-
- // Set up listener for texture updates
- const onAssetsUpdated = () => {
- void loadTextures()
- }
- appViewer.resourcesManager.on('assetsTexturesUpdated', onAssetsUpdated)
-
- // Cleanup
- return () => {
- appViewer.resourcesManager.off('assetsTexturesUpdated', onAssetsUpdated)
- // Cleanup texture URLs
- for (const url of fireTextures) URL.revokeObjectURL(url)
- }
- }, [])
-
- if (!onFire || fireTextures.length === 0 || perspective !== 'first_person') return null
-
- return (
-
- )
-}
diff --git a/src/react/FoodBar.css b/src/react/FoodBar.css
index 83da3c25..ca3629cd 100644
--- a/src/react/FoodBar.css
+++ b/src/react/FoodBar.css
@@ -11,6 +11,7 @@
--offset: calc(-1 * (52px + (9px * (4 * var(--kind) + var(--lightened) * 2))));
--bg-x: calc(-1 * (16px + 9px * var(--lightened)));
--bg-y: calc(-1 * 27px);
+ pointer-events: none;
image-rendering: pixelated;
}
diff --git a/src/react/FullscreenTime.tsx b/src/react/FullscreenTime.tsx
deleted file mode 100644
index 00b56740..00000000
--- a/src/react/FullscreenTime.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { useEffect, useState } from 'react'
-import { useSnapshot } from 'valtio'
-import { options } from '../optionsStorage'
-import PixelartIcon, { pixelartIcons } from './PixelartIcon'
-
-interface BatteryManager extends EventTarget {
- charging: boolean
- chargingTime: number
- dischargingTime: number
- level: number
- addEventListener(type: 'chargingchange' | 'levelchange', listener: EventListener): void
- removeEventListener(type: 'chargingchange' | 'levelchange', listener: EventListener): void
-}
-
-declare global {
- interface Navigator {
- getBattery(): Promise
- }
-}
-
-export default () => {
- const [fullScreen, setFullScreen] = useState(false)
- const { topRightTimeDisplay } = useSnapshot(options)
- if (topRightTimeDisplay === 'never') return null
- return
-}
-
-const FullscreenTime = () => {
- const { topRightTimeDisplay } = useSnapshot(options)
- const [fullScreen, setFullScreen] = useState(false)
- const [time, setTime] = useState('')
- const [batteryInfo, setBatteryInfo] = useState<{ level: number, charging: boolean } | null>(null)
-
- useEffect(() => {
- document.documentElement.addEventListener('fullscreenchange', () => {
- setFullScreen(!!document.fullscreenElement)
- })
-
- // Update time every second
- const updateTime = () => {
- const now = new Date()
- const hours = now.getHours().toString().padStart(2, '0')
- const minutes = now.getMinutes().toString().padStart(2, '0')
- setTime(`${hours}:${minutes}`)
- }
- updateTime()
- const timeInterval = setInterval(updateTime, 1000)
-
- // Get battery info if available
- if ('getBattery' in navigator) {
- void navigator.getBattery().then(battery => {
- const updateBatteryInfo = () => {
- setBatteryInfo({
- level: Math.round(battery.level * 100),
- charging: battery.charging
- })
- }
- updateBatteryInfo()
- battery.addEventListener('levelchange', updateBatteryInfo)
- battery.addEventListener('chargingchange', updateBatteryInfo)
- return () => {
- battery.removeEventListener('levelchange', updateBatteryInfo)
- battery.removeEventListener('chargingchange', updateBatteryInfo)
- }
- })
- }
-
- return () => {
- clearInterval(timeInterval)
- }
- }, [])
-
- if (topRightTimeDisplay === 'only-fullscreen' && !fullScreen) return null
-
- return (
-
-
{time}
- {batteryInfo && (
-
-
-
{batteryInfo.level}%
-
- )}
-
- )
-}
-
-const getBatteryIcon = (level: number, charging: boolean) => {
- if (charging) return pixelartIcons['battery-charging']
- if (level > 60) return pixelartIcons['battery-full']
- if (level > 20) return pixelartIcons['battery-2']
- if (level > 5) return pixelartIcons['battery-1']
- return pixelartIcons['battery']
-}
diff --git a/src/react/GameInteractionOverlay.tsx b/src/react/GameInteractionOverlay.tsx
index 268162ec..63cee586 100644
--- a/src/react/GameInteractionOverlay.tsx
+++ b/src/react/GameInteractionOverlay.tsx
@@ -2,7 +2,6 @@ import { useRef, useEffect } from 'react'
import { subscribe, useSnapshot } from 'valtio'
import { useUtilsEffect } from '@zardoy/react-util'
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
-import { isItemActivatableMobile } from 'mineflayer-mouse/dist/activatableItemsMobile'
import { options } from '../optionsStorage'
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls'
@@ -78,10 +77,7 @@ function GameInteractionOverlayInner ({
if (options.touchInteractionType === 'classic') {
virtualClickTimeout ??= setTimeout(() => {
virtualClickActive = true
- // If held item is activatable, use right click instead of left
- const heldItemName = bot?.heldItem?.name
- const isOnlyActivatable = heldItemName && isItemActivatableMobile(heldItemName, loadedData)
- document.dispatchEvent(new MouseEvent('mousedown', { button: isOnlyActivatable ? 2 : 0 }))
+ document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
}, touchStartBreakingBlockMs)
}
}
@@ -154,23 +150,16 @@ function GameInteractionOverlayInner ({
if (virtualClickActive) {
// button 0 is left click
- // If held item is activatable, use right click instead of left
- const heldItemName = bot?.heldItem?.name
- const isOnlyActivatable = heldItemName && isItemActivatableMobile(heldItemName, loadedData)
- document.dispatchEvent(new MouseEvent('mouseup', { button: isOnlyActivatable ? 2 : 0 }))
+ document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
virtualClickActive = false
} else if (!capturedPointer.active.activateCameraMove && (Date.now() - capturedPointer.active.time < touchStartBreakingBlockMs)) {
// single click action
const MOUSE_BUTTON_RIGHT = 2
const MOUSE_BUTTON_LEFT = 0
- const heldItemName = bot?.heldItem?.name
- const isOnlyActivatable = heldItemName && isItemActivatableMobile(heldItemName, loadedData)
const gonnaAttack = !!bot.mouse.getCursorState().entity || !!videoCursorInteraction()
- // If not attacking entity and item is activatable, use right click for breaking
- const useButton = !gonnaAttack && isOnlyActivatable ? MOUSE_BUTTON_RIGHT : (gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT)
- document.dispatchEvent(new MouseEvent('mousedown', { button: useButton }))
+ document.dispatchEvent(new MouseEvent('mousedown', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))
bot.mouse.update()
- document.dispatchEvent(new MouseEvent('mouseup', { button: useButton }))
+ document.dispatchEvent(new MouseEvent('mouseup', { button: gonnaAttack ? MOUSE_BUTTON_LEFT : MOUSE_BUTTON_RIGHT }))
}
if (screenTouches > 0) {
diff --git a/src/react/GlobalOverlayHints.tsx b/src/react/GlobalOverlayHints.tsx
deleted file mode 100644
index ddbb1116..00000000
--- a/src/react/GlobalOverlayHints.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { useEffect } from 'react'
-import { proxy, useSnapshot } from 'valtio'
-import { isInRealGameSession, pointerLock } from '../utils'
-import { activeModalStack, miscUiState } from '../globalState'
-import PixelartIcon, { pixelartIcons } from './PixelartIcon'
-import { useUsingTouch } from './utilsApp'
-
-export const displayHintsState = proxy({
- captureMouseHint: false
-})
-
-export default () => {
- const { captureMouseHint } = useSnapshot(displayHintsState)
- const { usingGamepadInput } = useSnapshot(miscUiState)
- const usingTouch = useUsingTouch()
- const acitveModals = useSnapshot(activeModalStack).length > 0
-
- const inRealGameSession = isInRealGameSession()
-
- useEffect(() => {
- const listener = () => {
- if (pointerLock.hasPointerLock) {
- displayHintsState.captureMouseHint = false
- }
- }
- document.addEventListener('pointerlockchange', listener)
-
- return () => {
- document.removeEventListener('pointerlockchange', listener)
- }
- }, [])
-
- return
- {captureMouseHint && !usingTouch && !usingGamepadInput && !acitveModals && inRealGameSession &&
-
-
{translate('Click to capture mouse')}
-
}
-
-}
diff --git a/src/react/HealthBar.css b/src/react/HealthBar.css
index 72f5a1fe..a6fefe09 100644
--- a/src/react/HealthBar.css
+++ b/src/react/HealthBar.css
@@ -11,6 +11,7 @@
--offset: calc(-1 * (52px + (9px * (4 * var(--kind) + var(--lightened) * 2)) ));
--bg-x: calc(-1 * (16px + 9px * var(--lightened)));
--bg-y: calc(-1 * var(--hardcore) * 45px);
+ pointer-events: none;
image-rendering: pixelated;
}
diff --git a/src/react/HotbarRenderApp.tsx b/src/react/HotbarRenderApp.tsx
index c782e6ef..e1eeb47a 100644
--- a/src/react/HotbarRenderApp.tsx
+++ b/src/react/HotbarRenderApp.tsx
@@ -1,23 +1,24 @@
import { useEffect, useRef, useState } from 'react'
-import { motion, AnimatePresence } from 'framer-motion'
+import { Transition } from 'react-transition-group'
import { createPortal } from 'react-dom'
import { subscribe, useSnapshot } from 'valtio'
-import { openItemsCanvas, upInventoryItems } from '../inventoryWindows'
+import { openItemsCanvas, openPlayerInventory, upInventoryItems } from '../inventoryWindows'
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
import { currentScaling } from '../scaleInterface'
import { watchUnloadForCleanup } from '../gameUnload'
import { getItemNameRaw } from '../mineflayer/items'
import { isInRealGameSession } from '../utils'
-import { triggerCommand } from '../controls'
import MessageFormattedString from './MessageFormattedString'
import SharedHudVars from './SharedHudVars'
+import { packetsReplayState } from './state/packetsReplayState'
const ItemName = ({ itemKey }: { itemKey: string }) => {
+ const nodeRef = useRef(null)
const [show, setShow] = useState(false)
const [itemName, setItemName] = useState | string>('')
- const duration = 0.3
+ const duration = 300
const defaultStyle: React.CSSProperties = {
position: 'fixed',
@@ -26,9 +27,18 @@ const ItemName = ({ itemKey }: { itemKey: string }) => {
right: 0,
fontSize: 10,
textAlign: 'center',
+ transition: `opacity ${duration}ms ease-in-out`,
+ opacity: 0,
pointerEvents: 'none',
}
+ const transitionStyles = {
+ entering: { opacity: 1 },
+ entered: { opacity: 1 },
+ exiting: { opacity: 0 },
+ exited: { opacity: 0 },
+ }
+
useEffect(() => {
const item = bot.heldItem
if (item) {
@@ -51,40 +61,26 @@ const ItemName = ({ itemKey }: { itemKey: string }) => {
}
}, [itemKey])
- return (
-
- {show && (
-
-
-
-
-
- )}
-
- )
+ return
+ {state => (
+
+
+
+
+
+ )}
+
}
const HotbarInner = () => {
const container = useRef(null!)
const [itemKey, setItemKey] = useState('')
const hasModals = useSnapshot(activeModalStack).length
- const { currentTouch, appConfig } = useSnapshot(miscUiState)
- const mobileOpenInventory = currentTouch && !appConfig?.disabledCommands?.includes('general.inventory')
useEffect(() => {
const controller = new AbortController()
const inv = openItemsCanvas('HotbarWin', {
- _client: {
- write () {}
- },
clickWindow (slot, mouseButton, mode) {
if (mouseButton === 1) {
console.log('right click')
@@ -107,27 +103,25 @@ const HotbarInner = () => {
canvasManager.setScale(currentScaling.scale)
canvasManager.windowHeight = 25 * canvasManager.scale
- canvasManager.windowWidth = (210 - (inv.inventory.supportsOffhand ? 0 : 25) + (mobileOpenInventory ? 28 : 0)) * canvasManager.scale
+ canvasManager.windowWidth = (210 - (inv.inventory.supportsOffhand ? 0 : 25) + (miscUiState.currentTouch ? 28 : 0)) * canvasManager.scale
}
setSize()
watchUnloadForCleanup(subscribe(currentScaling, setSize))
inv.canvas.style.pointerEvents = 'auto'
container.current.appendChild(inv.canvas)
const upHotbarItems = () => {
- if (!appViewer.resourcesManager?.itemsAtlasParser) return
- globalThis.debugHotbarItems = upInventoryItems(true, inv)
+ if (!appViewer.resourcesManager.currentResources?.itemsAtlasParser) return
+ upInventoryItems(true, inv)
}
canvasManager.canvas.onclick = (e) => {
if (!isGameActive(true)) return
const pos = inv.canvasManager.getMousePos(inv.canvas, e)
- if (canvasManager.canvas.width - pos.x < 35 * inv.canvasManager.scale && mobileOpenInventory) {
- triggerCommand('general.inventory', true)
- triggerCommand('general.inventory', false)
+ if (canvasManager.canvas.width - pos.x < 35 * inv.canvasManager.scale) {
+ openPlayerInventory()
}
}
- globalThis.debugUpHotbarItems = upHotbarItems
upHotbarItems()
bot.inventory.on('updateSlot', upHotbarItems)
appViewer.resourcesManager.on('assetsTexturesUpdated', upHotbarItems)
@@ -184,8 +178,17 @@ const HotbarInner = () => {
})
document.addEventListener('touchend', (e) => {
if (touchStart && (e.target as HTMLElement).closest('.hotbar') && Date.now() - touchStart > 700) {
- triggerCommand('general.dropStack', true)
- triggerCommand('general.dropStack', false)
+ // drop item
+ bot._client.write('block_dig', {
+ 'status': 4,
+ 'location': {
+ 'x': 0,
+ 'z': 0,
+ 'y': 0
+ },
+ 'face': 0,
+ sequence: 0
+ })
}
touchStart = 0
})
@@ -201,28 +204,17 @@ const HotbarInner = () => {
+ bottom: 'var(--hud-bottom-raw)'
+ }}
+ />
}
diff --git a/src/react/HudBarsProvider.tsx b/src/react/HudBarsProvider.tsx
index 42fa8378..db061500 100644
--- a/src/react/HudBarsProvider.tsx
+++ b/src/react/HudBarsProvider.tsx
@@ -1,7 +1,5 @@
import { useRef, useState, useMemo } from 'react'
import { GameMode } from 'mineflayer'
-import { useSnapshot } from 'valtio'
-import { options } from '../optionsStorage'
import { armor } from './armorValues'
import HealthBar from './HealthBar'
import FoodBar from './FoodBar'
@@ -10,8 +8,6 @@ import BreathBar from './BreathBar'
import './HealthBar.css'
export default () => {
- const { disabledUiParts } = useSnapshot(options)
-
const [damaged, setDamaged] = useState(false)
const [healthValue, setHealthValue] = useState(bot.health)
const [food, setFood] = useState(bot.food)
@@ -95,7 +91,7 @@ export default () => {
}, [])
return
- {!disabledUiParts.includes('health-bar') &&
{
setEffectToAdd(null)
setEffectToRemove(null)
}}
- />}
- {!disabledUiParts.includes('armor-bar') &&
+ }
- {!disabledUiParts.includes('food-bar') &&
+ {
setEffectToAdd(null)
setEffectToRemove(null)
}}
- />}
- {!disabledUiParts.includes('breath-bar') &&
+ }
+ />
}
diff --git a/src/react/IndicatorEffects.css b/src/react/IndicatorEffects.css
index 797b37e8..6b178c58 100644
--- a/src/react/IndicatorEffects.css
+++ b/src/react/IndicatorEffects.css
@@ -1,9 +1,9 @@
-.indicators-container-outer {
+.effectsScreen-container {
position: fixed;
top: max(6%, 30px);
- left: calc(env(safe-area-inset-left) / 2);
+ left: 0px;
z-index: -2;
- /* pointer-events: none; */
+ pointer-events: none;
}
.indicators-container {
@@ -17,68 +17,19 @@
}
.effect-box {
- position: relative;
display: flex;
align-items: center;
- background: rgba(40, 40, 40, 0.85);
- border-top: 1px solid #9f9f9f;
- border-left: 1px solid #9f9f9f;
- border-right: 1px solid #373737;
- border-bottom: 1px solid #373737;
- padding: 3px 9px 3px 4px;
- margin-bottom: 3px;
- min-width: 60px;
- min-height: 22px;
- box-sizing: border-box;
- overflow: hidden;
-}
-
-.effect-box__progress-bg {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- background: linear-gradient(90deg, #b2b2b2 0%, #6b6b6b 100%);
- opacity: 0.18;
- z-index: 0;
- transition: width 0.3s linear;
}
.effect-box__image {
- width: 16px;
- height: 16px;
- margin-right: 4px;
- z-index: 1;
-}
-
-.effect-box__content {
- display: flex;
- flex-direction: column;
- justify-content: center;
- z-index: 1;
- flex: 1;
-}
-
-.effect-box__title {
- color: #e0e0e0;
- font-size: 0.45rem;
- font-weight: 600;
- margin-bottom: 1px;
- text-shadow: 1px 1px 0 #222, 0 0 2px #000;
+ width: 23px;
+ margin-right: 3px;
}
.effect-box__time {
- color: #fff;
- font-size: 0.5rem;
- text-shadow: 1px 1px 0 #222, 0 0 2px #000;
+ font-size: 0.65rem;
}
.effect-box__level {
- position: absolute;
- right: 4px;
- top: 4px;
- color: #fff;
- font-size: 0.48rem;
- text-shadow: 1px 1px 0 #222, 0 0 2px #000;
- z-index: 2;
+ font-size: 0.45rem;
}
diff --git a/src/react/IndicatorEffects.stories.tsx b/src/react/IndicatorEffects.stories.tsx
new file mode 100644
index 00000000..e0371550
--- /dev/null
+++ b/src/react/IndicatorEffects.stories.tsx
@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import IndicatorEffects, { defaultIndicatorsState } from './IndicatorEffects'
+import { images } from './effectsImages'
+
+const meta: Meta = {
+ component: IndicatorEffects
+}
+
+export default meta
+type Story = StoryObj
+
+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) { }
+ }
+ ],
+ }
+}
diff --git a/src/react/IndicatorEffects.tsx b/src/react/IndicatorEffects.tsx
index 50ece8d5..5b05290f 100644
--- a/src/react/IndicatorEffects.tsx
+++ b/src/react/IndicatorEffects.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useEffect, useRef, useState } from 'react'
+import { useMemo, useEffect, useRef } from 'react'
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
import './IndicatorEffects.css'
@@ -9,55 +9,35 @@ function formatTime (seconds: number): string {
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.floor(seconds % 60)
const formattedMinutes = String(minutes).padStart(2, '0')
- const formattedSeconds = String(remainingSeconds).padStart(2, '0')
+ const formattedSeconds = String(remainingSeconds)
return `${formattedMinutes}:${formattedSeconds}`
}
export type EffectType = {
- id: number,
image: string,
+ time: number,
level: number,
- initialTime: number,
- duration: number,
- name: string,
+ removeEffect: (image: string) => void,
+ reduceTime: (image: string) => void
}
-const EffectBox = ({ image, level, name, initialTime, duration }: Pick) => {
- const [currentTime, setCurrentTime] = useState(Date.now())
+const EffectBox = ({ image, time, level }: Pick) => {
- useEffect(() => {
- const interval = setInterval(() => {
- setCurrentTime(Date.now())
- }, 100)
- return () => clearInterval(interval)
- }, [])
+ const formattedTime = useMemo(() => formatTime(time), [time])
- const timeElapsed = (currentTime - initialTime) / 1000
- const timeRemaining = Math.max(0, duration - timeElapsed)
- const progress = duration > 0 ? Math.max(0, Math.min(1, timeRemaining / duration)) : 0
- const formattedTime = useMemo(() => formatTime(timeRemaining), [timeRemaining])
-
- // Convert level to Roman numerals
- const toRomanNumeral = (num: number): string => {
- if (num <= 0) return ''
- const romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X']
- return romanNumerals[num - 1] || `${num}`
- }
-
- const levelText = level > 0 && level < 256 ? ` ${toRomanNumeral(level + 1)}` : ''
-
- return (
-
-
-

-
-
{name}{levelText}
- {formattedTime && (
-
{formattedTime}
- )}
-
+ return
+

+
+ {formattedTime ? (
+ // if time is negative then effect is shown without time.
+ // Component should be removed manually with time = 0
+
{formattedTime}
+ ) : null}
+ {level > 0 && level < 256 ? (
+
{level + 1}
+ ) : null}
- )
+
}
export const defaultIndicatorsState = {
@@ -88,17 +68,29 @@ const colorOverrides = {
}
}
-export default ({
- indicators,
- effects,
- displayIndicators,
- displayEffects
-}: {
- indicators: typeof defaultIndicatorsState,
- effects: readonly EffectType[]
- displayIndicators: boolean
- displayEffects: boolean
-}) => {
+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]) => {
const state = indicators[key]
return {
@@ -108,10 +100,10 @@ export default ({
key
}
})
- return
+ return
{
- displayIndicators && indicatorsMapped.map((indicator) =>
)
}
- {displayEffects &&
}
-
-}
-
-const EffectsInner = ({ effects }: { effects: readonly EffectType[] }) => {
- return
- {effects.map((effect) => (
-
- ))}
+
+ {
+ effects.map((effect) => )
+ }
+
}
diff --git a/src/react/IndicatorEffectsProvider.tsx b/src/react/IndicatorEffectsProvider.tsx
index df52296a..097bb28d 100644
--- a/src/react/IndicatorEffectsProvider.tsx
+++ b/src/react/IndicatorEffectsProvider.tsx
@@ -1,10 +1,10 @@
import { proxy, subscribe, useSnapshot } from 'valtio'
import { useEffect, useMemo, useState } from 'react'
import { subscribeKey } from 'valtio/utils'
-import { Effect } from 'mineflayer'
import { inGameError } from '../utils'
import { fsState } from '../loadSave'
import { gameAdditionalState, miscUiState } from '../globalState'
+import { options } from '../optionsStorage'
import IndicatorEffects, { EffectType, defaultIndicatorsState } from './IndicatorEffects'
import { images } from './effectsImages'
@@ -14,58 +14,50 @@ export const state = proxy({
effects: [] as EffectType[]
})
-export const addEffect = (newEffect: Effect) => {
- const effectData = loadedData.effectsArray.find(e => e.id === newEffect.id)
- const name = effectData?.name ?? `unknown: ${newEffect.id}`
- const nameKebab = name.replaceAll(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).slice(1)
- const image = images[nameKebab] ?? null
- if (!image) {
- inGameError(`received unknown effect id ${newEffect.id}`)
- return
- }
-
- const effectIndex = getEffectIndex({ id: newEffect.id })
+export const addEffect = (newEffect: Omit
) => {
+ const effectIndex = getEffectIndex(newEffect as EffectType)
if (typeof effectIndex === 'number') {
- state.effects[effectIndex].initialTime = Date.now()
- state.effects[effectIndex].level = newEffect.amplifier
- state.effects[effectIndex].duration = newEffect.duration / 20 // convert ticks to seconds
+ state.effects[effectIndex].time = newEffect.time
+ state.effects[effectIndex].level = newEffect.level
} else {
- const effect: EffectType = {
- id: newEffect.id,
- name,
- image,
- level: newEffect.amplifier,
- initialTime: Date.now(),
- duration: newEffect.duration / 20, // convert ticks to seconds
- }
+ const effect = { ...newEffect, reduceTime, removeEffect }
state.effects.push(effect)
}
}
-const removeEffect = (id: number) => {
+const removeEffect = (image: string) => {
for (const [index, effect] of (state.effects).entries()) {
- if (effect.id === id) {
+ if (effect.image === image) {
state.effects.splice(index, 1)
}
}
}
-const getEffectIndex = (newEffect: Pick) => {
+const reduceTime = (image: string) => {
for (const [index, effect] of (state.effects).entries()) {
- if (effect.id === newEffect.id) {
+ 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 ({ displayEffects = true, displayIndicators = true }: { displayEffects?: boolean, displayIndicators?: boolean }) => {
+export default () => {
const [dummyState, setDummyState] = useState(false)
const stateIndicators = useSnapshot(state.indicators)
const chunksLoading = !useSnapshot(appViewer.rendererState).world.allChunksLoaded
const { mesherWork } = useSnapshot(appViewer.rendererState).world
const { hasErrors } = useSnapshot(miscUiState)
+ const { disabledUiParts } = useSnapshot(options)
const { isReadonly, openReadOperations, openWriteOperations } = useSnapshot(fsState)
const { noConnection, poorConnection } = useSnapshot(gameAdditionalState)
const allIndicators: typeof defaultIndicatorsState = {
@@ -95,32 +87,33 @@ export default ({ displayEffects = true, displayIndicators = true }: { displayEf
const nameKebab = effect.name.replaceAll(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).slice(1)
return [effect.id, images[nameKebab]]
}))
- const gotEffect = (entity: import('prismarine-entity').Entity, effect: Effect) => {
+ bot.on('entityEffect', (entity, effect) => {
if (entity.id !== bot.entity.id) return
- addEffect(effect)
- }
- bot.on('entityEffect', gotEffect)
-
- // gotEffect(bot.entity, {
- // id: 1,
- // amplifier: 1,
- // duration: 100,
- // })
-
- for (const effect of Object.values(bot.entity.effects ?? {})) {
- gotEffect(bot.entity, effect)
- }
-
+ 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
- removeEffect(effect.id)
+ const image = effectsImages[effect.id] ?? null
+ if (!image) {
+ inGameError(`received unknown effect id ${effect.id}}}`)
+ return
+ }
+ removeEffect(image)
})
}, [])
return
}
diff --git a/src/react/Input.tsx b/src/react/Input.tsx
index 9b36c5ce..169e880d 100644
--- a/src/react/Input.tsx
+++ b/src/react/Input.tsx
@@ -35,6 +35,7 @@ const Input = ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, w
return