196 lines
6.9 KiB
JavaScript
196 lines
6.9 KiB
JavaScript
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 (
|
|
<div style={{ position: 'relative', width: '100vw', height: '100vh', background: '#000' }}>
|
|
{/* NASA iframe layer - fully interactive */}
|
|
<iframe
|
|
src={NASA_URL}
|
|
style={{
|
|
...layerStyle,
|
|
zIndex: isNasa ? 10 : -1,
|
|
border: 'none',
|
|
opacity: isNasa ? 1 : 0,
|
|
transition: 'opacity 1.5s ease',
|
|
pointerEvents: isNasa ? 'auto' : 'none',
|
|
}}
|
|
title="NASA Solar System"
|
|
allowFullScreen
|
|
allow="accelerometer; autoplay; encrypted-media; gyroscope"
|
|
/>
|
|
{/* NASA center button */}
|
|
{isNasa && (
|
|
<button
|
|
onClick={() => { lastSwitchRef.current = Date.now(); setView('avatar'); }}
|
|
onMouseEnter={() => { lastSwitchRef.current = Date.now(); setView('avatar'); }}
|
|
onTouchStart={() => { lastSwitchRef.current = Date.now(); setView('avatar'); }}
|
|
style={{
|
|
position: 'absolute', top: '50%', left: '50%',
|
|
transform: 'translate(-50%, -50%)', zIndex: 100,
|
|
width: 40, height: 40, borderRadius: '50%',
|
|
background: 'rgba(255,255,255,0.08)', border: '1px solid rgba(255,255,255,0.15)',
|
|
cursor: 'pointer', pointerEvents: 'auto',
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Atmosphere background */}
|
|
<div style={{ ...layerStyle, zIndex: 0, opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
|
<Canvas gl={{ alpha: true, antialias: true }}>
|
|
<AtmosphereScene timeScale={timeScale} />
|
|
</Canvas>
|
|
</div>
|
|
|
|
{/* Avatar layer */}
|
|
<div style={{ ...layerStyle, zIndex: 1, pointerEvents: 'none', opacity: isNasa ? 0 : 1, transition: 'opacity 1.5s ease' }}>
|
|
<div style={{ width: '100%', height: '100%', pointerEvents: isNasa ? 'none' : 'auto' }}>
|
|
<AvatarScene selectedAnimation={animState} onZoomOut={handleZoomOut} camSpeed={camSpeed} vrmModel={vrmModel} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Control panel */}
|
|
{!isNasa && (
|
|
<ControlPanel
|
|
timeScale={timeScale}
|
|
onTimeScaleChange={setTimeScale}
|
|
camSpeed={camSpeed}
|
|
onCamSpeedChange={setCamSpeed}
|
|
onSkill={doSkill}
|
|
/>
|
|
)}
|
|
|
|
{/* Admin controls */}
|
|
{isAdmin && !isNasa && (
|
|
<div style={{ position: 'absolute', top: 16, left: 16, zIndex: 2, display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
<button style={btnStyle} onClick={doTeleport}>Teleport</button>
|
|
<button style={btnStyle} onClick={doSkill}>Skill</button>
|
|
<span style={{ color: '#0f0', fontSize: '12px', marginLeft: 4 }}>ADMIN</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|