diff --git a/src/App.jsx b/src/App.jsx index f8d1fdf..a6ada8f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,7 +3,7 @@ import { Canvas } from '@react-three/fiber'; import AtmosphereScene from './AtmosphereScene'; import AvatarScene from './AvatarScene'; -import { LOCATIONS, teleportTo } from './worldState'; +import { LOCATIONS, teleportTo, worldState } from './worldState'; import { adminMode, onAdminChange } from './controls/KeyInput'; import ControlPanel from './ui/ControlPanel'; @@ -23,6 +23,13 @@ export default function App() { const [lang, setLang] = useState('en'); const [volume, setVolume] = useState(0); 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 volumeRef = useRef(0); const voiceIndexRef = useRef(0); @@ -162,6 +169,50 @@ export default function App() { setBurstSky(null); if (windRef.current) { windRef.current.play().catch(() => {}); } }, 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 file = `${import.meta.env.BASE_URL}voice/ai/burst_1${suffix}.mp3`; const audio = new Audio(file); @@ -270,6 +321,114 @@ export default function App() { /> )} + {/* Score */} + {!isNasa && score > 0 && ( +