import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Canvas } from '@react-three/fiber'; import AtmosphereScene from './AtmosphereScene'; import AvatarScene from './AvatarScene'; import { LOCATIONS, teleportTo } from './worldState'; import { adminMode, onAdminChange } from './controls/KeyInput'; import ControlPanel from './ui/ControlPanel'; const ACTION_SEQUENCE = ['attack', 'skill', 'jump', 'fly_dodge', 'damage']; const TELEPORT_ANIM = 'fly_dodge'; const AUTO_INTERVAL_MIN = 15000; const AUTO_INTERVAL_MAX = 40000; const NASA_URL = 'https://eyes.nasa.gov/apps/solar-system/#/earth?featured=false&detailPanel=false&logo=false&search=false&shareButton=false&menu=false&collapseSettingsOptions=true&hideFullScreenToggle=true'; export default function App() { const [animState, setAnimState] = useState({ name: 'fly_idle', count: 0 }); const [isAdmin, setIsAdmin] = useState(false); const [view, setView] = useState('avatar'); // 'avatar' | 'nasa' const [timeScale, setTimeScale] = useState(100); const [camSpeed, setCamSpeed] = useState(0.05); const [vrmModel, setVrmModel] = useState('ai.vrm'); const actionIndexRef = useRef(0); const teleportIndexRef = useRef(0); const countRef = useRef(0); useEffect(() => { return onAdminChange((v) => setIsAdmin(v)); }, []); const playAnim = useCallback((name) => { countRef.current += 1; setAnimState({ name, count: countRef.current }); }, []); const handleKey = useCallback((e) => { if (e.code === 'Escape') { setView(view === 'nasa' ? 'avatar' : 'nasa'); return; } if (e.code === 'Space') { e.preventDefault(); const idx = actionIndexRef.current; const anim = ACTION_SEQUENCE[idx % ACTION_SEQUENCE.length]; playAnim(anim); actionIndexRef.current = idx + 1; } else if (e.code === 'KeyT') { e.preventDefault(); const idx = teleportIndexRef.current; const loc = LOCATIONS[idx % LOCATIONS.length]; playAnim(TELEPORT_ANIM); teleportTo(loc); teleportIndexRef.current = idx + 1; } else if (e.code === 'KeyS') { e.preventDefault(); playAnim('skill'); setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm'); } }, [playAnim, view]); useEffect(() => { window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); }, [handleKey]); const viewRef = useRef(view); viewRef.current = view; useEffect(() => { const scheduleNext = () => { const delay = AUTO_INTERVAL_MIN + Math.random() * (AUTO_INTERVAL_MAX - AUTO_INTERVAL_MIN); return setTimeout(() => { // NASAビュー中はスキップ if (viewRef.current !== 'nasa') { const idx = teleportIndexRef.current; const loc = LOCATIONS[idx % LOCATIONS.length]; playAnim(TELEPORT_ANIM); teleportTo(loc); teleportIndexRef.current = idx + 1; } timerRef.current = scheduleNext(); }, delay); }; const timerRef = { current: scheduleNext() }; return () => clearTimeout(timerRef.current); }, [playAnim]); const doTeleport = useCallback(() => { const idx = teleportIndexRef.current; const loc = LOCATIONS[idx % LOCATIONS.length]; playAnim(TELEPORT_ANIM); teleportTo(loc); teleportIndexRef.current = idx + 1; }, [playAnim]); const doSkill = useCallback(() => { playAnim('skill'); setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm'); }, [playAnim]); const appStartRef = useRef(Date.now()); const lastSwitchRef = useRef(0); const handleZoomOut = useCallback(() => { const now = Date.now(); // 起動後15秒、または切り替え後5秒はブロック if (now - appStartRef.current < 15000) return; if (now - lastSwitchRef.current < 5000) return; lastSwitchRef.current = now; setView('nasa'); }, []); const layerStyle = { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%' }; const btnStyle = { padding: '8px 16px', background: 'rgba(255,255,255,0.12)', color: '#fff', border: '1px solid rgba(255,255,255,0.25)', borderRadius: '6px', cursor: 'pointer', fontSize: '13px', backdropFilter: 'blur(4px)', }; const isNasa = view === 'nasa'; return (
{/* NASA iframe layer - fully interactive */}