add game
This commit is contained in:
161
src/App.jsx
161
src/App.jsx
@@ -3,7 +3,7 @@ import { Canvas } from '@react-three/fiber';
|
|||||||
|
|
||||||
import AtmosphereScene from './AtmosphereScene';
|
import AtmosphereScene from './AtmosphereScene';
|
||||||
import AvatarScene from './AvatarScene';
|
import AvatarScene from './AvatarScene';
|
||||||
import { LOCATIONS, teleportTo } from './worldState';
|
import { LOCATIONS, teleportTo, worldState } from './worldState';
|
||||||
import { adminMode, onAdminChange } from './controls/KeyInput';
|
import { adminMode, onAdminChange } from './controls/KeyInput';
|
||||||
import ControlPanel from './ui/ControlPanel';
|
import ControlPanel from './ui/ControlPanel';
|
||||||
|
|
||||||
@@ -23,6 +23,13 @@ export default function App() {
|
|||||||
const [lang, setLang] = useState('en');
|
const [lang, setLang] = useState('en');
|
||||||
const [volume, setVolume] = useState(0);
|
const [volume, setVolume] = useState(0);
|
||||||
const [burstSky, setBurstSky] = useState(null);
|
const [burstSky, setBurstSky] = useState(null);
|
||||||
|
const [damageNum, setDamageNum] = useState(null);
|
||||||
|
const [score, setScore] = useState(0);
|
||||||
|
const scoreTimerRef = useRef(null);
|
||||||
|
const [scoreTimeLeft, setScoreTimeLeft] = useState(0);
|
||||||
|
const scoreEndRef = useRef(0);
|
||||||
|
const scoreRafRef = useRef(null);
|
||||||
|
const [crowns, setCrowns] = useState({ bronze: false, silver: false, gold: false });
|
||||||
const langRef = useRef('en');
|
const langRef = useRef('en');
|
||||||
const volumeRef = useRef(0);
|
const volumeRef = useRef(0);
|
||||||
const voiceIndexRef = useRef(0);
|
const voiceIndexRef = useRef(0);
|
||||||
@@ -162,6 +169,50 @@ export default function App() {
|
|||||||
setBurstSky(null);
|
setBurstSky(null);
|
||||||
if (windRef.current) { windRef.current.play().catch(() => {}); }
|
if (windRef.current) { windRef.current.play().catch(() => {}); }
|
||||||
}, 4500);
|
}, 4500);
|
||||||
|
// Damage number - show during blackhole phase
|
||||||
|
setTimeout(() => {
|
||||||
|
const baseDmg = Math.floor(Math.random() * 100000) + 1;
|
||||||
|
// x2 if ai_mode
|
||||||
|
const modeMult = vrmModel === 'ai_mode.vrm' ? 2 : 1;
|
||||||
|
// x1-x12 based on sun position (noon=x12, midnight=x1)
|
||||||
|
const hour = worldState.currentHour;
|
||||||
|
const hourMult = Math.max(1, Math.round(12 - Math.abs(hour - 12)) + 1);
|
||||||
|
// x1-x3 based on location
|
||||||
|
const locMult = { Tokyo: 1, Fuji: 2, Space: 3 }[worldState.currentLocation] || 1;
|
||||||
|
const dmg = baseDmg * modeMult * hourMult * locMult;
|
||||||
|
const x = 40 + Math.random() * 20;
|
||||||
|
const y = 35 + Math.random() * 20;
|
||||||
|
setDamageNum({ value: dmg, x, y });
|
||||||
|
setTimeout(() => setDamageNum(null), 1500);
|
||||||
|
// Score system
|
||||||
|
setScore(prev => {
|
||||||
|
const newScore = prev + dmg;
|
||||||
|
// Reset timer: 10s after first damage
|
||||||
|
// Only start timer on first damage
|
||||||
|
if (!scoreTimerRef.current) {
|
||||||
|
scoreEndRef.current = Date.now() + 20000;
|
||||||
|
const tick = () => {
|
||||||
|
const left = Math.max(0, Math.ceil((scoreEndRef.current - Date.now()) / 1000));
|
||||||
|
setScoreTimeLeft(left);
|
||||||
|
if (left > 0) scoreRafRef.current = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
scoreTimerRef.current = setTimeout(() => {
|
||||||
|
setScore(prev => {
|
||||||
|
setCrowns(c => ({
|
||||||
|
bronze: c.bronze || prev >= 100,
|
||||||
|
silver: c.silver || prev >= 100000,
|
||||||
|
gold: c.gold || prev >= 1000000,
|
||||||
|
}));
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
setScoreTimeLeft(0);
|
||||||
|
scoreTimerRef.current = null;
|
||||||
|
}, 20000);
|
||||||
|
}
|
||||||
|
return newScore;
|
||||||
|
});
|
||||||
|
}, 2500);
|
||||||
const suffix = langRef.current === 'en' ? '_en' : '';
|
const suffix = langRef.current === 'en' ? '_en' : '';
|
||||||
const file = `${import.meta.env.BASE_URL}voice/ai/burst_1${suffix}.mp3`;
|
const file = `${import.meta.env.BASE_URL}voice/ai/burst_1${suffix}.mp3`;
|
||||||
const audio = new Audio(file);
|
const audio = new Audio(file);
|
||||||
@@ -270,6 +321,114 @@ export default function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Score */}
|
||||||
|
{!isNasa && score > 0 && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16,
|
||||||
|
left: 16,
|
||||||
|
zIndex: 3,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 8,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
}}>
|
||||||
|
SCORE {score}
|
||||||
|
</span>
|
||||||
|
{scoreTimeLeft > 0 && (
|
||||||
|
<span style={{
|
||||||
|
padding: '2px 8px',
|
||||||
|
background: 'rgba(255,255,255,0.1)',
|
||||||
|
border: '1px solid rgba(255,255,255,0.2)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: 'rgba(255, 255, 255, 0.5)',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
}}>
|
||||||
|
{scoreTimeLeft}s
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Damage number */}
|
||||||
|
{damageNum && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: `${damageNum.x}%`,
|
||||||
|
top: `${damageNum.y}%`,
|
||||||
|
zIndex: 50,
|
||||||
|
color: 'rgba(255, 220, 50, 0.75)',
|
||||||
|
fontSize: '48px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
textShadow: '0 0 10px rgba(255, 180, 0, 0.5)',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
animation: 'dmgFade 1.5s ease-out forwards',
|
||||||
|
}}>
|
||||||
|
{damageNum.value}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<style>{`
|
||||||
|
@keyframes dmgFade {
|
||||||
|
0% { opacity: 1; transform: translateY(0) scale(1); }
|
||||||
|
100% { opacity: 0; transform: translateY(-40px) scale(1.2); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Clear crowns - gold(top), silver, bronze(bottom) */}
|
||||||
|
{!isNasa && (crowns.bronze || crowns.silver || crowns.gold) && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 16,
|
||||||
|
right: 16,
|
||||||
|
zIndex: 3,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
background: crowns.gold ? 'rgba(255,255,255,0.1)' : 'transparent',
|
||||||
|
border: crowns.gold ? '1px solid rgba(255,255,255,0.2)' : '1px solid transparent',
|
||||||
|
borderRadius: '4px',
|
||||||
|
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{crowns.gold && (
|
||||||
|
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="gold" style={{ width: 20, height: 20, filter: 'brightness(0) invert(1) sepia(1) saturate(10) hue-rotate(330deg)' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
background: crowns.silver ? 'rgba(255,255,255,0.1)' : 'transparent',
|
||||||
|
border: crowns.silver ? '1px solid rgba(255,255,255,0.2)' : '1px solid transparent',
|
||||||
|
borderRadius: '4px',
|
||||||
|
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{crowns.silver && (
|
||||||
|
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="silver" style={{ width: 20, height: 20, filter: 'brightness(0) invert(1) brightness(0.9)' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: '4px 8px',
|
||||||
|
background: crowns.bronze ? 'rgba(255,255,255,0.1)' : 'transparent',
|
||||||
|
border: crowns.bronze ? '1px solid rgba(255,255,255,0.2)' : '1px solid transparent',
|
||||||
|
borderRadius: '4px',
|
||||||
|
width: 28, height: 28, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{crowns.bronze && (
|
||||||
|
<img src={`${import.meta.env.BASE_URL}icon/crown.svg`} alt="bronze" style={{ width: 20, height: 20, filter: 'brightness(0) invert(1) sepia(1) saturate(10) hue-rotate(90deg)' }} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Admin controls */}
|
{/* Admin controls */}
|
||||||
{isAdmin && !isNasa && (
|
{isAdmin && !isNasa && (
|
||||||
<div style={{ position: 'absolute', top: 16, left: 16, zIndex: 2, display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ position: 'absolute', top: 16, left: 16, zIndex: 2, display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader';
|
||||||
|
|
||||||
import { FollowCamera } from './controls/CameraRig';
|
import { FollowCamera } from './controls/CameraRig';
|
||||||
|
import { worldState } from './worldState';
|
||||||
|
|
||||||
const TIME_SCALE = 100;
|
const TIME_SCALE = 100;
|
||||||
const INITIAL_DATE = new Date('2024-06-21T12:00:00');
|
const INITIAL_DATE = new Date('2024-06-21T12:00:00');
|
||||||
@@ -111,6 +112,7 @@ export default function AtmosphereScene({ timeScale: timeScaleProp, overrideDate
|
|||||||
useFrame((_, delta) => {
|
useFrame((_, delta) => {
|
||||||
const currentDate = dateRef.current;
|
const currentDate = dateRef.current;
|
||||||
currentDate.setTime(currentDate.getTime() + delta * timeScaleRef.current * 1000);
|
currentDate.setTime(currentDate.getTime() + delta * timeScaleRef.current * 1000);
|
||||||
|
worldState.currentHour = currentDate.getHours() + currentDate.getMinutes() / 60;
|
||||||
|
|
||||||
if (atmosphereRef.current) {
|
if (atmosphereRef.current) {
|
||||||
atmosphereRef.current.updateByDate(currentDate);
|
atmosphereRef.current.updateByDate(currentDate);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { worldState } from './worldState';
|
|||||||
|
|
||||||
const STAGE_ROTATE_SPEED = 0.1; // キャラ周回速度
|
const STAGE_ROTATE_SPEED = 0.1; // キャラ周回速度
|
||||||
|
|
||||||
const BREATH_NEAR = 1.8; // カメラ最近距離
|
const BREATH_NEAR = 0.5; // カメラ最近距離
|
||||||
const BREATH_FAR = 4.0; // カメラ最遠距離
|
const BREATH_FAR = 4.0; // カメラ最遠距離
|
||||||
const BREATH_SPEED = 0.35; // 寄り引き速度
|
const BREATH_SPEED = 0.35; // 寄り引き速度
|
||||||
const BREATH_HEIGHT_RATIO = 0.4; // 距離に対する上下移動の割合
|
const BREATH_HEIGHT_RATIO = 0.4; // 距離に対する上下移動の割合
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export const worldState = {
|
|||||||
quaternion: new Quaternion(),
|
quaternion: new Quaternion(),
|
||||||
speed: 1000.0,
|
speed: 1000.0,
|
||||||
stageRotationY: 0,
|
stageRotationY: 0,
|
||||||
|
currentHour: 12,
|
||||||
|
currentLocation: 'Tokyo',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pre-allocated temp objects (avoid GC in useFrame)
|
// Pre-allocated temp objects (avoid GC in useFrame)
|
||||||
@@ -49,7 +51,10 @@ export const LOCATIONS = [
|
|||||||
{ name: 'Space', longitude: 139.7671, latitude: 35.6812, heading: 0, pitch: -90, distance: 100000 },
|
{ name: 'Space', longitude: 139.7671, latitude: 35.6812, heading: 0, pitch: -90, distance: 100000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const LOCATION_MULT = { Tokyo: 1, Fuji: 2, Space: 3 };
|
||||||
|
|
||||||
export function teleportTo(location) {
|
export function teleportTo(location) {
|
||||||
|
worldState.currentLocation = location.name;
|
||||||
const { longitude, latitude, heading, pitch, distance } = location;
|
const { longitude, latitude, heading, pitch, distance } = location;
|
||||||
const position = new Vector3();
|
const position = new Vector3();
|
||||||
const globalQuaternion = new Quaternion();
|
const globalQuaternion = new Quaternion();
|
||||||
|
|||||||
Reference in New Issue
Block a user