1
0
Files
vrm/src/App.jsx
2026-03-07 14:35:30 +09:00

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>
);
}