feat: add idle yawn animation when user is inactive (F-4)
After 30s without user input on Elements tab, producer sprites cycle a subtle yawn (translateY + scale) every 8s. Resets immediately on any pointer/touch/key/scroll event. Implemented via wrapper div so CharacterSprite internals are untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<ElementData | null>(null);
|
||||
const [activityLog, setActivityLog] = useState<ActivityEntry[]>([]);
|
||||
const { items: floatItems, add: addFloat } = useFloatingItems(1100);
|
||||
@@ -1651,13 +1669,15 @@ export function ElementsScreen() {
|
||||
}}
|
||||
>
|
||||
<div css={producerSpriteStyle(active)}>
|
||||
<CharacterSprite
|
||||
elementId={el.id}
|
||||
elementColor={el.color}
|
||||
tier={el.tier}
|
||||
size={42}
|
||||
state={(elements[el.id] ?? 0) > 0 ? 'obtained' : 'locked'}
|
||||
/>
|
||||
<div css={idleSpriteWrapperStyle(idleMood)}>
|
||||
<CharacterSprite
|
||||
elementId={el.id}
|
||||
elementColor={el.color}
|
||||
tier={el.tier}
|
||||
size={42}
|
||||
state={(elements[el.id] ?? 0) > 0 ? 'obtained' : 'locked'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div css={producerInfoStyle}>
|
||||
<div css={producerNameStyle}>
|
||||
|
||||
39
src/lib/useIdleMood.ts
Normal file
39
src/lib/useIdleMood.ts
Normal file
@@ -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<keyof WindowEventMap> = [
|
||||
'pointerdown',
|
||||
'touchstart',
|
||||
'keydown',
|
||||
'scroll',
|
||||
];
|
||||
|
||||
export function useIdleMood(idleAfterMs: number = IDLE_AFTER_MS): Mood {
|
||||
const [mood, setMood] = useState<Mood>('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;
|
||||
}
|
||||
Reference in New Issue
Block a user