init
This commit is contained in:
174
src/App.jsx
Normal file
174
src/App.jsx
Normal file
@@ -0,0 +1,174 @@
|
||||
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';
|
||||
|
||||
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&locked=true&hideExternalLinks=true&lighting=flood';
|
||||
|
||||
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 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');
|
||||
}
|
||||
}, [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');
|
||||
}, [playAnim]);
|
||||
|
||||
const appStartRef = useRef(Date.now());
|
||||
const handleZoomOut = useCallback(() => {
|
||||
// 起動後15秒は切り替え無効
|
||||
if (Date.now() - appStartRef.current < 15000) return;
|
||||
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={() => setView('avatar')}
|
||||
onMouseEnter={() => setView('avatar')}
|
||||
onTouchStart={() => 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 />
|
||||
</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} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user