diff --git a/src/components/screens/ElementsScreen.tsx b/src/components/screens/ElementsScreen.tsx index 4692afd..4a392e6 100644 --- a/src/components/screens/ElementsScreen.tsx +++ b/src/components/screens/ElementsScreen.tsx @@ -6,6 +6,7 @@ import { useFloatingItems } from '../../hooks/useFloatingItems'; import elementsData from '../../data/elements.json'; import recipesData from '../../data/recipes.json'; import { CharacterSprite } from '../CharacterSprite'; +import { useIdleMood, type Mood } from '../../lib/useIdleMood'; import { calcResonanceBonuses, calcTierAutomationStatus, @@ -515,6 +516,22 @@ const producerSpriteStyle = (active: boolean) => css` animation: ${active ? `${scenePulse} 1.8s ease-in-out infinite` : 'none'}; `; +const idleYawnKeyframes = keyframes` + 0%, 70%, 100% { transform: translateY(0) scale(1); opacity: 1; } + 78% { transform: translateY(-1.5px) scale(1.04, 0.96); opacity: 0.94; } + 88% { transform: translateY(-2.5px) scale(1.06, 0.94); opacity: 0.88; } + 96% { transform: translateY(-0.5px) scale(1.02, 0.99); opacity: 0.97; } +`; + +const idleSpriteWrapperStyle = (mood: Mood) => css` + display: inline-block; + width: 100%; + height: 100%; + ${mood === 'idle' + ? `animation: ${idleYawnKeyframes.toString()} 8s ease-in-out infinite;` + : ''} +`; + const producerInfoStyle = css` min-width: 0; `; @@ -1325,6 +1342,7 @@ export function ElementsScreen() { const claimDailyBonus = useGameStore((s) => s.claimDailyBonus); const claimDailyMission = useGameStore((s) => s.claimDailyMission); const harvestElement = useGameStore((s) => s.harvestElement); + const idleMood = useIdleMood(); const [selectedEl, setSelectedEl] = useState(null); const [activityLog, setActivityLog] = useState([]); const { items: floatItems, add: addFloat } = useFloatingItems(1100); @@ -1651,13 +1669,15 @@ export function ElementsScreen() { }} >
- 0 ? 'obtained' : 'locked'} - /> +
+ 0 ? 'obtained' : 'locked'} + /> +
diff --git a/src/lib/useIdleMood.ts b/src/lib/useIdleMood.ts new file mode 100644 index 0000000..3a35a25 --- /dev/null +++ b/src/lib/useIdleMood.ts @@ -0,0 +1,39 @@ +import { useEffect, useState } from 'react'; + +const IDLE_AFTER_MS = 30_000; +const TICK_MS = 2_000; + +export type Mood = 'awake' | 'idle'; + +const ACTIVITY_EVENTS: Array = [ + 'pointerdown', + 'touchstart', + 'keydown', + 'scroll', +]; + +export function useIdleMood(idleAfterMs: number = IDLE_AFTER_MS): Mood { + const [mood, setMood] = useState('awake'); + + useEffect(() => { + let lastActivity = Date.now(); + const onActivity = () => { + lastActivity = Date.now(); + setMood((prev) => (prev === 'awake' ? prev : 'awake')); + }; + ACTIVITY_EVENTS.forEach((evt) => + window.addEventListener(evt, onActivity, { passive: true }) + ); + const tick = window.setInterval(() => { + if (Date.now() - lastActivity >= idleAfterMs) { + setMood((prev) => (prev === 'idle' ? prev : 'idle')); + } + }, TICK_MS); + return () => { + ACTIVITY_EVENTS.forEach((evt) => window.removeEventListener(evt, onActivity)); + window.clearInterval(tick); + }; + }, [idleAfterMs]); + + return mood; +}