diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue index 9007bb269..84e7fd0b4 100644 --- a/apps/app-frontend/src/pages/Skins.vue +++ b/apps/app-frontend/src/pages/Skins.vue @@ -22,7 +22,7 @@ import { computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch } import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue' import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue' import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue' -import { handleError } from '@/store/notifications' +import { handleError, useNotifications } from '@/store/notifications' import type { Cape, Skin } from '@/helpers/skins.ts' import { normalize_skin_texture, @@ -48,6 +48,8 @@ const editSkinModal = useTemplateRef('editSkinModal') const selectCapeModal = useTemplateRef('selectCapeModal') const uploadSkinModal = useTemplateRef('uploadSkinModal') +const notifications = useNotifications() + const settings = ref(await getSettings()) const skins = ref([]) const capes = ref([]) @@ -149,7 +151,16 @@ async function changeSkin(newSkin: Skin) { } catch (error) { selectedSkin.value = previousSkin skins.value = previousSkinsList - handleError(error) + + if ((error as { message?: string })?.message?.includes('429 Too Many Requests')) { + notifications.addNotification({ + type: 'error', + title: 'Slow down!', + text: "You're changing your skin too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.", + }) + } else { + handleError(error) + } } } @@ -169,7 +180,16 @@ async function handleCapeSelected(cape: Cape | undefined) { } catch (error) { defaultCape.value = previousDefaultCape capes.value = previousCapesList - handleError(error) + + if ((error as { message?: string })?.message?.includes('429 Too Many Requests')) { + notifications.addNotification({ + type: 'error', + title: 'Slow down!', + text: "You're changing your cape too frequently. Mojang's servers have temporarily blocked further requests. Please wait a moment before trying again.", + }) + } else { + handleError(error) + } } } diff --git a/packages/ui/src/components/skin/SkinPreviewRenderer.vue b/packages/ui/src/components/skin/SkinPreviewRenderer.vue index 0376b8ad6..fd59be4a4 100644 --- a/packages/ui/src/components/skin/SkinPreviewRenderer.vue +++ b/packages/ui/src/components/skin/SkinPreviewRenderer.vue @@ -150,7 +150,7 @@ const props = withDefaults( baseAnimation: 'idle', randomAnimations: ['idle_sub_1', 'idle_sub_2', 'idle_sub_3'], randomAnimationInterval: 8000, - transitionDuration: 0.5, + transitionDuration: 0.2, }), }, ) @@ -204,11 +204,14 @@ function initializeAnimations(loadedScene: THREE.Object3D, clips: THREE.Animatio clips.forEach((clip) => { const action = mixer.value!.clipAction(clip) + + action.setLoop(THREE.LoopOnce, 1) + action.clampWhenFinished = true actions.value[clip.name] = action - console.log(`Loaded animation: ${clip.name}`) }) if (baseAnimation.value && actions.value[baseAnimation.value]) { + actions.value[baseAnimation.value].setLoop(THREE.LoopRepeat, Infinity) playAnimation(baseAnimation.value) setupRandomAnimationLoop() } else { @@ -216,7 +219,7 @@ function initializeAnimations(loadedScene: THREE.Object3D, clips: THREE.Animatio const firstAnimationName = Object.keys(actions.value)[0] if (firstAnimationName) { - console.log(`Playing first available animation: ${firstAnimationName}`) + actions.value[firstAnimationName].setLoop(THREE.LoopRepeat, Infinity) playAnimation(firstAnimationName) } } @@ -229,6 +232,12 @@ function playAnimation(name: string) { } const action = actions.value[name] + + if (currentAnimation.value === name && action.isRunning() && name !== baseAnimation.value) { + console.log(`Animation "${name}" is already running, ignoring request`) + return false + } + const transitionDuration = props.animationConfig.transitionDuration || 0.3 Object.entries(actions.value).forEach(([actionName, actionInstance]) => { @@ -238,12 +247,35 @@ function playAnimation(name: string) { }) action.reset() - action.setLoop(THREE.LoopRepeat, Infinity) + + if (name === baseAnimation.value) { + action.setLoop(THREE.LoopRepeat, Infinity) + } else { + action.setLoop(THREE.LoopOnce, 1) + action.clampWhenFinished = true + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onFinished = (event: any) => { + if (event.action === action) { + mixer.value?.removeEventListener('finished', onFinished) + if (currentAnimation.value === name && baseAnimation.value) { + action.fadeOut(transitionDuration) + const baseAction = actions.value[baseAnimation.value] + baseAction.reset() + baseAction.fadeIn(transitionDuration) + baseAction.play() + currentAnimation.value = baseAnimation.value + } + } + } + + mixer.value.addEventListener('finished', onFinished) + } + action.fadeIn(transitionDuration) action.play() currentAnimation.value = name - console.log(`Playing animation: ${name}`) return true } @@ -265,15 +297,48 @@ function setupRandomAnimationLoop() { } function playRandomAnimation(name: string) { - if (!playAnimation(name)) return + if (!mixer.value || !actions.value[name]) { + console.warn(`Animation "${name}" not found!`) + return + } - const animationDuration = actions.value[name].getClip().duration * 1000 + const action = actions.value[name] - setTimeout(() => { - if (currentAnimation.value === name && baseAnimation.value) { - playAnimation(baseAnimation.value) + if (currentAnimation.value === name && action.isRunning()) { + console.log(`Animation "${name}" is already running, ignoring request`) + return + } + + const transitionDuration = props.animationConfig.transitionDuration || 0.3 + + if (baseAnimation.value && actions.value[baseAnimation.value].isRunning()) { + actions.value[baseAnimation.value].fadeOut(transitionDuration) + } + + action.reset() + action.setLoop(THREE.LoopOnce, 1) + action.clampWhenFinished = true + action.fadeIn(transitionDuration) + action.play() + + currentAnimation.value = name + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onFinished = (event: any) => { + if (event.action === action) { + mixer.value?.removeEventListener('finished', onFinished) + if (currentAnimation.value === name && baseAnimation.value) { + action.fadeOut(transitionDuration) + const baseAction = actions.value[baseAnimation.value] + baseAction.reset() + baseAction.fadeIn(transitionDuration) + baseAction.play() + currentAnimation.value = baseAnimation.value + } } - }, animationDuration + 500) + } + + mixer.value.addEventListener('finished', onFinished) } function stopAnimations() {