1
0
This commit is contained in:
2026-03-07 23:16:35 +09:00
parent 350b32e87f
commit 03e1ac06bf
4 changed files with 168 additions and 2 deletions

View File

@@ -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' }}>

View File

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

View File

@@ -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; // 距離に対する上下移動の割合

View File

@@ -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();