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 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 && (
|
||||
<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 */}
|
||||
{isAdmin && !isNasa && (
|
||||
<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 { FollowCamera } from './controls/CameraRig';
|
||||
import { worldState } from './worldState';
|
||||
|
||||
const TIME_SCALE = 100;
|
||||
const INITIAL_DATE = new Date('2024-06-21T12:00:00');
|
||||
@@ -111,6 +112,7 @@ export default function AtmosphereScene({ timeScale: timeScaleProp, overrideDate
|
||||
useFrame((_, delta) => {
|
||||
const currentDate = dateRef.current;
|
||||
currentDate.setTime(currentDate.getTime() + delta * timeScaleRef.current * 1000);
|
||||
worldState.currentHour = currentDate.getHours() + currentDate.getMinutes() / 60;
|
||||
|
||||
if (atmosphereRef.current) {
|
||||
atmosphereRef.current.updateByDate(currentDate);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { worldState } from './worldState';
|
||||
|
||||
const STAGE_ROTATE_SPEED = 0.1; // キャラ周回速度
|
||||
|
||||
const BREATH_NEAR = 1.8; // カメラ最近距離
|
||||
const BREATH_NEAR = 0.5; // カメラ最近距離
|
||||
const BREATH_FAR = 4.0; // カメラ最遠距離
|
||||
const BREATH_SPEED = 0.35; // 寄り引き速度
|
||||
const BREATH_HEIGHT_RATIO = 0.4; // 距離に対する上下移動の割合
|
||||
|
||||
@@ -8,6 +8,8 @@ export const worldState = {
|
||||
quaternion: new Quaternion(),
|
||||
speed: 1000.0,
|
||||
stageRotationY: 0,
|
||||
currentHour: 12,
|
||||
currentLocation: 'Tokyo',
|
||||
};
|
||||
|
||||
// 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 },
|
||||
];
|
||||
|
||||
const LOCATION_MULT = { Tokyo: 1, Fuji: 2, Space: 3 };
|
||||
|
||||
export function teleportTo(location) {
|
||||
worldState.currentLocation = location.name;
|
||||
const { longitude, latitude, heading, pitch, distance } = location;
|
||||
const position = new Vector3();
|
||||
const globalQuaternion = new Quaternion();
|
||||
|
||||
Reference in New Issue
Block a user