diff --git a/src/App.jsx b/src/App.jsx index 264b37a..3efef2f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -20,6 +20,10 @@ export default function App() { const [timeScale, setTimeScale] = useState(100); const [camSpeed, setCamSpeed] = useState(0.05); const [vrmModel, setVrmModel] = useState('ai.vrm'); + const [lang, setLang] = useState('ja'); + const langRef = useRef('ja'); + const voiceIndexRef = useRef(0); + const voicePattern = ['normal','normal','normal','normal','normal','normal','normal','normal','skill','skill']; const actionIndexRef = useRef(0); const teleportIndexRef = useRef(0); const countRef = useRef(0); @@ -28,11 +32,25 @@ export default function App() { return onAdminChange((v) => setIsAdmin(v)); }, []); + const handleLangChange = useCallback((v) => { setLang(v); langRef.current = v; }, []); + + const playSkillVoice = useCallback(() => { + const type = voicePattern[voiceIndexRef.current % voicePattern.length]; + voiceIndexRef.current += 1; + const suffix = langRef.current === 'en' ? '_en' : ''; + const file = `${import.meta.env.BASE_URL}voice/ai/${type}_1${suffix}.mp3`; + const audio = new Audio(file); + audio.volume = 0.7; + audio.play().catch(() => {}); + }, []); + const playAnim = useCallback((name) => { countRef.current += 1; setAnimState({ name, count: countRef.current }); }, []); + const doSkillRef = useRef(null); + const handleKey = useCallback((e) => { if (e.code === 'Escape') { setView(view === 'nasa' ? 'avatar' : 'nasa'); @@ -53,8 +71,7 @@ export default function App() { teleportIndexRef.current = idx + 1; } else if (e.code === 'KeyS') { e.preventDefault(); - playAnim('skill'); - setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm'); + doSkillRef.current?.(); } }, [playAnim, view]); @@ -93,10 +110,16 @@ export default function App() { teleportIndexRef.current = idx + 1; }, [playAnim]); + const skillCoolRef = useRef(0); const doSkill = useCallback(() => { + const now = Date.now(); + if (now - skillCoolRef.current < 3000) return; + skillCoolRef.current = now; playAnim('skill'); + playSkillVoice(); setVrmModel(prev => prev === 'ai.vrm' ? 'ai_mode.vrm' : 'ai.vrm'); - }, [playAnim]); + }, [playAnim, playSkillVoice]); + doSkillRef.current = doSkill; const appStartRef = useRef(Date.now()); const lastSwitchRef = useRef(0); @@ -179,6 +202,8 @@ export default function App() { camSpeed={camSpeed} onCamSpeedChange={setCamSpeed} onSkill={doSkill} + lang={lang} + onLangChange={handleLangChange} /> )} diff --git a/src/VrmCharacter.jsx b/src/VrmCharacter.jsx index 2fae135..cd30ad7 100644 --- a/src/VrmCharacter.jsx +++ b/src/VrmCharacter.jsx @@ -73,6 +73,14 @@ export default function VrmCharacter({ selectedAnimation: animState, vrmModel = const vrm = gltf.userData.vrm; vrmRef.current = vrm; VRMUtils.removeUnnecessaryJoints(vrm.scene); + // SpringBone: centerをRotatingStage(親group)に設定し、回転に追従させる + const sbm = vrm.springBoneManager; + if (sbm) { + const stageGroup = vrm.scene.parent; + sbm.joints?.forEach(joint => { + if (stageGroup) joint.center = stageGroup; + }); + } vrm.humanoid.resetPose(); vrm.scene.rotation.y = Math.PI; diff --git a/src/ui/ControlPanel.jsx b/src/ui/ControlPanel.jsx index db47537..b65e7b5 100644 --- a/src/ui/ControlPanel.jsx +++ b/src/ui/ControlPanel.jsx @@ -44,7 +44,7 @@ const btnStyle = { width: '100%', }; -export default function ControlPanel({ timeScale, onTimeScaleChange, camSpeed, onCamSpeedChange, onSkill }) { +export default function ControlPanel({ timeScale, onTimeScaleChange, camSpeed, onCamSpeedChange, onSkill, lang, onLangChange }) { return (
@@ -77,8 +77,19 @@ export default function ControlPanel({ timeScale, onTimeScaleChange, camSpeed, o style={sliderStyle} />
- +
);