855 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			855 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * 虚拟猫头鹰老师模块
 | |
|  * 提供3D猫头鹰角色、语音朗读、状态管理等功能
 | |
|  */
 | |
| class VirtualTeacher {
 | |
|     constructor(containerId = 'virtual-teacher-container') {
 | |
|         this.containerId = containerId;
 | |
|         this.container = null;
 | |
|         this.scene = null;
 | |
|         this.camera = null;
 | |
|         this.renderer = null;
 | |
|         this.owl = null;
 | |
|         this.isSpeaking = false;
 | |
|         this.animationId = null;
 | |
|         this.currentExpression = "normal";
 | |
|         this.speechSynthesis = window.speechSynthesis;
 | |
|         this.currentUtterance = null;
 | |
|         this.isInitialized = false;
 | |
|         this.speechQueue = [];
 | |
|         this.isPlaying = false;
 | |
|         
 | |
|         // 状态管理
 | |
|         this.gameStats = {
 | |
|             correct: 0,
 | |
|             incorrect: 0,
 | |
|             total: 0,
 | |
|             streak: 0,
 | |
|             maxStreak: 0
 | |
|         };
 | |
|         
 | |
|         // 表情和状态映射
 | |
|         this.expressions = {
 | |
|             normal: { name: "正常", emoji: "😐" },
 | |
|             happy: { name: "开心", emoji: "😊" },
 | |
|             excited: { name: "兴奋", emoji: "🤩" },
 | |
|             thinking: { name: "思考", emoji: "🤔" },
 | |
|             surprised: { name: "惊讶", ememoji: "😮" },
 | |
|             encouraging: { name: "鼓励", emoji: "💪" },
 | |
|             proud: { name: "骄傲", emoji: "🦉" }
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 初始化虚拟老师
 | |
|      */
 | |
|     async init() {
 | |
|         if (this.isInitialized) return;
 | |
|         
 | |
|         try {
 | |
|             // 创建容器
 | |
|             this.createContainer();
 | |
|             
 | |
|             // 初始化Three.js
 | |
|             await this.initThreeJS();
 | |
|             
 | |
|             // 添加交互
 | |
|             this.addInteractions();
 | |
|             
 | |
|             this.isInitialized = true;
 | |
|             console.log('🦉 虚拟猫头鹰老师初始化完成');
 | |
|             
 | |
|             // 显示欢迎消息
 | |
|             this.showWelcomeMessage();
 | |
|             
 | |
|         } catch (error) {
 | |
|             console.error('虚拟老师初始化失败:', error);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 创建容器
 | |
|      */
 | |
|     createContainer() {
 | |
|         // 检查是否已存在容器
 | |
|         let container = document.getElementById(this.containerId);
 | |
|         if (!container) {
 | |
|             container = document.createElement('div');
 | |
|             container.id = this.containerId;
 | |
|             container.className = 'virtual-teacher-container';
 | |
|             document.body.appendChild(container);
 | |
|         }
 | |
|         
 | |
|         this.container = container;
 | |
|         
 | |
|         // 添加样式
 | |
|         this.addStyles();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 添加样式
 | |
|      */
 | |
|     addStyles() {
 | |
|         if (document.getElementById('virtual-teacher-styles')) return;
 | |
|         
 | |
|         const style = document.createElement('style');
 | |
|         style.id = 'virtual-teacher-styles';
 | |
|         style.textContent = `
 | |
|             .virtual-teacher-container {
 | |
|                 position: fixed;
 | |
|                 top: 20px;
 | |
|                 right: 20px;
 | |
|                 width: 200px;
 | |
|                 height: 200px;
 | |
|                 z-index: 1000;
 | |
|                 border-radius: 15px;
 | |
|                 border: 3px solid #d2691e;
 | |
|                 box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
 | |
|                 background: linear-gradient(135deg, #f0e68c 0%, #daa520 100%);
 | |
|                 overflow: hidden;
 | |
|                 transition: transform 0.3s ease;
 | |
|             }
 | |
|             
 | |
|             .virtual-teacher-container:hover {
 | |
|                 transform: scale(1.05);
 | |
|             }
 | |
|             
 | |
|             .virtual-teacher-container.speaking {
 | |
|                 animation: teacherGlow 2s infinite;
 | |
|             }
 | |
|             
 | |
|             @keyframes teacherGlow {
 | |
|                 0% { box-shadow: 0 0 0 0 rgba(210, 105, 30, 0.7); }
 | |
|                 70% { box-shadow: 0 0 0 15px rgba(210, 105, 30, 0); }
 | |
|                 100% { box-shadow: 0 0 0 0 rgba(210, 105, 30, 0); }
 | |
|             }
 | |
|             
 | |
|             .teacher-speech-bubble {
 | |
|                 position: absolute;
 | |
|                 bottom: -80px;
 | |
|                 left: 50%;
 | |
|                 transform: translateX(-50%);
 | |
|                 background: linear-gradient(135deg, #ff8c42 0%, #ff6b35 100%);
 | |
|                 color: white;
 | |
|                 border-radius: 15px;
 | |
|                 padding: 10px 15px;
 | |
|                 max-width: 250px;
 | |
|                 text-align: center;
 | |
|                 font-size: 12px;
 | |
|                 line-height: 1.4;
 | |
|                 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
 | |
|                 opacity: 0;
 | |
|                 transition: all 0.5s ease;
 | |
|                 z-index: 1001;
 | |
|             }
 | |
|             
 | |
|             .teacher-speech-bubble:after {
 | |
|                 content: "";
 | |
|                 position: absolute;
 | |
|                 top: -8px;
 | |
|                 left: 50%;
 | |
|                 margin-left: -8px;
 | |
|                 border-width: 8px;
 | |
|                 border-style: solid;
 | |
|                 border-color: transparent transparent #ff6b35 transparent;
 | |
|             }
 | |
|             
 | |
|             .teacher-speech-bubble.visible {
 | |
|                 opacity: 1;
 | |
|                 transform: translateX(-50%) translateY(-5px);
 | |
|             }
 | |
|             
 | |
|             .teacher-status {
 | |
|                 position: absolute;
 | |
|                 top: -30px;
 | |
|                 left: 50%;
 | |
|                 transform: translateX(-50%);
 | |
|                 background: rgba(0, 0, 0, 0.7);
 | |
|                 color: white;
 | |
|                 padding: 5px 10px;
 | |
|                 border-radius: 10px;
 | |
|                 font-size: 11px;
 | |
|                 white-space: nowrap;
 | |
|             }
 | |
|         `;
 | |
|         document.head.appendChild(style);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 初始化Three.js场景
 | |
|      */
 | |
|     async initThreeJS() {
 | |
|         // 检查Three.js是否已加载
 | |
|         if (typeof THREE === 'undefined') {
 | |
|             await this.loadThreeJS();
 | |
|         }
 | |
| 
 | |
|         // 创建场景
 | |
|         this.scene = new THREE.Scene();
 | |
|         this.scene.background = new THREE.Color(0xf0e68c);
 | |
| 
 | |
|         // 创建相机
 | |
|         this.camera = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 1000);
 | |
|         this.camera.position.z = 100;
 | |
| 
 | |
|         // 创建渲染器
 | |
|         this.renderer = new THREE.WebGLRenderer({ antialias: true });
 | |
|         this.renderer.setSize(200, 200);
 | |
|         this.renderer.setClearColor(0xf0e68c);
 | |
|         this.container.appendChild(this.renderer.domElement);
 | |
| 
 | |
|         // 创建猫头鹰
 | |
|         this.createOwl();
 | |
| 
 | |
|         // 创建状态显示
 | |
|         this.createStatusDisplay();
 | |
| 
 | |
|         // 创建语音气泡
 | |
|         this.createSpeechBubble();
 | |
| 
 | |
|         // 开始动画循环
 | |
|         this.animate();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 加载Three.js库
 | |
|      */
 | |
|     loadThreeJS() {
 | |
|         return new Promise((resolve, reject) => {
 | |
|             if (typeof THREE !== 'undefined') {
 | |
|                 resolve();
 | |
|                 return;
 | |
|             }
 | |
|             
 | |
|             const script = document.createElement('script');
 | |
|             script.src = 'https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js';
 | |
|             script.onload = resolve;
 | |
|             script.onerror = reject;
 | |
|             document.head.appendChild(script);
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 创建猫头鹰角色
 | |
|      */
 | |
|     createOwl() {
 | |
|         this.owl = new THREE.Group();
 | |
| 
 | |
|         // 身体
 | |
|         const bodyGeometry = new THREE.CircleGeometry(40, 32);
 | |
|         const bodyMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xd2691e,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
|         const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
 | |
|         body.position.y = -20;
 | |
|         body.scale.set(1, 1.3, 1);
 | |
|         this.owl.add(body);
 | |
| 
 | |
|         // 肚子
 | |
|         const bellyGeometry = new THREE.CircleGeometry(28, 32);
 | |
|         const bellyMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xfff8dc,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
|         const belly = new THREE.Mesh(bellyGeometry, bellyMaterial);
 | |
|         belly.position.set(0, -22, 1);
 | |
|         belly.scale.set(1, 1.2, 1);
 | |
|         this.owl.add(belly);
 | |
| 
 | |
|         // 头部
 | |
|         const headGeometry = new THREE.CircleGeometry(35, 32);
 | |
|         const headMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xd2691e,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
|         const head = new THREE.Mesh(headGeometry, headMaterial);
 | |
|         head.position.y = 15;
 | |
|         this.owl.add(head);
 | |
| 
 | |
|         // 脸部
 | |
|         const faceGeometry = new THREE.CircleGeometry(25, 32);
 | |
|         const faceMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xfff8dc,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
|         const face = new THREE.Mesh(faceGeometry, faceMaterial);
 | |
|         face.position.set(0, 12, 1);
 | |
|         face.scale.set(1, 1.1, 1);
 | |
|         this.owl.add(face);
 | |
| 
 | |
|         // 眼睛外圈
 | |
|         const eyeOuterGeometry = new THREE.CircleGeometry(18, 32);
 | |
|         const eyeOuterMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xffffff,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
| 
 | |
|         const leftEyeOuter = new THREE.Mesh(eyeOuterGeometry, eyeOuterMaterial);
 | |
|         leftEyeOuter.position.set(-12, 20, 2);
 | |
|         this.owl.add(leftEyeOuter);
 | |
| 
 | |
|         const rightEyeOuter = new THREE.Mesh(eyeOuterGeometry, eyeOuterMaterial);
 | |
|         rightEyeOuter.position.set(12, 20, 2);
 | |
|         this.owl.add(rightEyeOuter);
 | |
| 
 | |
|         // 眼睛内圈
 | |
|         const eyeInnerGeometry = new THREE.CircleGeometry(14, 32);
 | |
|         const eyeInnerMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0x000000,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
| 
 | |
|         const leftEyeInner = new THREE.Mesh(eyeInnerGeometry, eyeInnerMaterial);
 | |
|         leftEyeInner.position.set(-12, 20, 3);
 | |
|         this.owl.add(leftEyeInner);
 | |
| 
 | |
|         const rightEyeInner = new THREE.Mesh(eyeInnerGeometry, eyeInnerMaterial);
 | |
|         rightEyeInner.position.set(12, 20, 3);
 | |
|         this.owl.add(rightEyeInner);
 | |
| 
 | |
|         // 瞳孔
 | |
|         const pupilGeometry = new THREE.CircleGeometry(10, 16);
 | |
|         const pupilMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0x8b4513,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
| 
 | |
|         const leftPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
 | |
|         leftPupil.position.set(-12, 20, 4);
 | |
|         this.owl.add(leftPupil);
 | |
| 
 | |
|         const rightPupil = new THREE.Mesh(pupilGeometry, pupilMaterial);
 | |
|         rightPupil.position.set(12, 20, 4);
 | |
|         this.owl.add(rightPupil);
 | |
| 
 | |
|         // 眼睛高光
 | |
|         const highlightGeometry = new THREE.CircleGeometry(4, 8);
 | |
|         const highlightMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xffffff,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
| 
 | |
|         const leftHighlight = new THREE.Mesh(highlightGeometry, highlightMaterial);
 | |
|         leftHighlight.position.set(-10, 22, 5);
 | |
|         this.owl.add(leftHighlight);
 | |
| 
 | |
|         const rightHighlight = new THREE.Mesh(highlightGeometry, highlightMaterial);
 | |
|         rightHighlight.position.set(10, 22, 5);
 | |
|         this.owl.add(rightHighlight);
 | |
| 
 | |
|         // 喙
 | |
|         const beakGeometry = new THREE.CircleGeometry(4, 8);
 | |
|         const beakMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xff8c00,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
|         const beak = new THREE.Mesh(beakGeometry, beakMaterial);
 | |
|         beak.position.set(0, 8, 3);
 | |
|         beak.scale.set(1, 0.8, 1);
 | |
|         this.owl.add(beak);
 | |
| 
 | |
|         // 翅膀
 | |
|         const wingGeometry = new THREE.CircleGeometry(22, 16);
 | |
|         const wingMaterial = new THREE.MeshBasicMaterial({
 | |
|             color: 0xcd853f,
 | |
|             side: THREE.DoubleSide,
 | |
|         });
 | |
| 
 | |
|         const leftWing = new THREE.Mesh(wingGeometry, wingMaterial);
 | |
|         leftWing.position.set(-42, -15, 0);
 | |
|         leftWing.scale.set(0.8, 1.4, 1);
 | |
|         leftWing.rotation.z = 0.3;
 | |
|         this.owl.add(leftWing);
 | |
| 
 | |
|         const rightWing = new THREE.Mesh(wingGeometry, wingMaterial);
 | |
|         rightWing.position.set(42, -15, 0);
 | |
|         rightWing.scale.set(0.8, 1.4, 1);
 | |
|         rightWing.rotation.z = -0.3;
 | |
|         this.owl.add(rightWing);
 | |
| 
 | |
|         // 存储可动画的部分
 | |
|         this.owl.userData = {
 | |
|             head: head,
 | |
|             body: body,
 | |
|             leftEyeOuter: leftEyeOuter,
 | |
|             rightEyeOuter: rightEyeOuter,
 | |
|             leftEyeInner: leftEyeInner,
 | |
|             rightEyeInner: rightEyeInner,
 | |
|             leftPupil: leftPupil,
 | |
|             rightPupil: rightPupil,
 | |
|             leftHighlight: leftHighlight,
 | |
|             rightHighlight: rightHighlight,
 | |
|             beak: beak,
 | |
|             leftWing: leftWing,
 | |
|             rightWing: rightWing,
 | |
|             blinkTimer: 0,
 | |
|             speakTimer: 0,
 | |
|             idleTimer: 0,
 | |
|         };
 | |
| 
 | |
|         this.scene.add(this.owl);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 创建状态显示
 | |
|      */
 | |
|     createStatusDisplay() {
 | |
|         const statusDiv = document.createElement('div');
 | |
|         statusDiv.className = 'teacher-status';
 | |
|         statusDiv.id = 'teacher-status';
 | |
|         statusDiv.textContent = '🦉 老师待机中...';
 | |
|         this.container.appendChild(statusDiv);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 创建语音气泡
 | |
|      */
 | |
|     createSpeechBubble() {
 | |
|         const bubbleDiv = document.createElement('div');
 | |
|         bubbleDiv.className = 'teacher-speech-bubble';
 | |
|         bubbleDiv.id = 'teacher-speech-bubble';
 | |
|         bubbleDiv.textContent = '你好!我是你的猫头鹰老师!';
 | |
|         this.container.appendChild(bubbleDiv);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 动画循环
 | |
|      */
 | |
|     animate() {
 | |
|         this.animationId = requestAnimationFrame(() => this.animate());
 | |
| 
 | |
|         if (this.owl && this.owl.userData) {
 | |
|             const userData = this.owl.userData;
 | |
| 
 | |
|             // 眨眼动画
 | |
|             userData.blinkTimer += 0.016;
 | |
|             if (userData.blinkTimer > 4) {
 | |
|                 userData.leftEyeOuter.scale.y = 0.1;
 | |
|                 userData.rightEyeOuter.scale.y = 0.1;
 | |
|                 userData.leftEyeInner.scale.y = 0.1;
 | |
|                 userData.rightEyeInner.scale.y = 0.1;
 | |
|                 if (userData.blinkTimer > 4.2) {
 | |
|                     userData.leftEyeOuter.scale.y = 1;
 | |
|                     userData.rightEyeOuter.scale.y = 1;
 | |
|                     userData.leftEyeInner.scale.y = 1;
 | |
|                     userData.rightEyeInner.scale.y = 1;
 | |
|                     userData.blinkTimer = 0;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // 说话动画
 | |
|             if (this.isSpeaking) {
 | |
|                 userData.speakTimer += 0.2;
 | |
|                 userData.beak.scale.y = 0.8 + Math.sin(userData.speakTimer) * 0.3;
 | |
|                 userData.head.position.y = 15 + Math.sin(userData.speakTimer * 0.7) * 2;
 | |
| 
 | |
|                 // 说话时翅膀轻微扇动
 | |
|                 userData.leftWing.rotation.z = 0.3 + Math.sin(userData.speakTimer * 0.5) * 0.1;
 | |
|                 userData.rightWing.rotation.z = -0.3 - Math.sin(userData.speakTimer * 0.5) * 0.1;
 | |
|             } else {
 | |
|                 userData.beak.scale.y = 0.8;
 | |
|                 userData.head.position.y = 15;
 | |
|                 userData.leftWing.rotation.z = 0.3;
 | |
|                 userData.rightWing.rotation.z = -0.3;
 | |
|             }
 | |
| 
 | |
|             // 闲置动画
 | |
|             userData.idleTimer += 0.008;
 | |
|             this.owl.rotation.z = Math.sin(userData.idleTimer) * 0.02;
 | |
|             userData.head.rotation.z = Math.sin(userData.idleTimer * 0.7) * 0.05;
 | |
|         }
 | |
| 
 | |
|         this.renderer.render(this.scene, this.camera);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 添加交互
 | |
|      */
 | |
|     addInteractions() {
 | |
|         this.container.addEventListener('click', () => {
 | |
|             this.blink();
 | |
|         });
 | |
| 
 | |
|         this.container.addEventListener('mouseenter', () => {
 | |
|             this.container.style.transform = 'scale(1.05)';
 | |
|         });
 | |
| 
 | |
|         this.container.addEventListener('mouseleave', () => {
 | |
|             this.container.style.transform = 'scale(1)';
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 显示欢迎消息
 | |
|      */
 | |
|     showWelcomeMessage() {
 | |
|         const messages = [
 | |
|             '你好!我是你的猫头鹰老师!',
 | |
|             '准备好开始学习了吗?',
 | |
|             '让我们一起探索知识的奥秘!',
 | |
|             '智慧就在我们身边!'
 | |
|         ];
 | |
|         
 | |
|         const randomMessage = messages[Math.floor(Math.random() * messages.length)];
 | |
|         this.speak(randomMessage);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 语音朗读
 | |
|      */
 | |
|     speak(text, options = {}) {
 | |
|         if (!text) return;
 | |
| 
 | |
|         // 立即停止所有当前语音
 | |
|         this.stopAllSpeech();
 | |
| 
 | |
|         // 显示文字气泡
 | |
|         this.showSpeechBubble(text);
 | |
| 
 | |
|         // 创建语音
 | |
|         this.currentUtterance = new SpeechSynthesisUtterance(text);
 | |
|         this.currentUtterance.lang = options.lang || 'zh-CN';
 | |
|         this.currentUtterance.rate = options.rate || 0.9;
 | |
|         this.currentUtterance.pitch = options.pitch || 1.1;
 | |
|         this.currentUtterance.volume = options.volume || 0.8;
 | |
| 
 | |
|         // 语音事件
 | |
|         this.currentUtterance.onstart = () => {
 | |
|             this.isPlaying = true;
 | |
|             // 设置全局状态,通知无障碍语音播报
 | |
|             window.virtualTeacherPlaying = true;
 | |
|             this.startSpeaking();
 | |
|             this.updateStatus('📚 正在讲解...');
 | |
|         };
 | |
| 
 | |
|         this.currentUtterance.onend = () => {
 | |
|             this.isPlaying = false;
 | |
|             // 清除全局状态
 | |
|             window.virtualTeacherPlaying = false;
 | |
|             this.stopSpeaking();
 | |
|             this.updateStatus('✨ 讲解完成');
 | |
|             setTimeout(() => {
 | |
|                 this.hideSpeechBubble();
 | |
|             }, 2000);
 | |
|         };
 | |
| 
 | |
|         this.currentUtterance.onerror = (event) => {
 | |
|             this.isPlaying = false;
 | |
|             // 清除全局状态
 | |
|             window.virtualTeacherPlaying = false;
 | |
|             this.stopSpeaking();
 | |
|             this.updateStatus(`❌ 语音出错: ${event.error}`);
 | |
|             console.error('语音合成错误:', event);
 | |
|         };
 | |
| 
 | |
|         // 开始语音
 | |
|         this.speechSynthesis.speak(this.currentUtterance);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 停止所有语音播放
 | |
|      */
 | |
|     stopAllSpeech() {
 | |
|         // 停止当前语音合成
 | |
|         if (this.speechSynthesis.speaking) {
 | |
|             this.speechSynthesis.cancel();
 | |
|         }
 | |
|         
 | |
|         // 清空语音队列
 | |
|         this.speechQueue = [];
 | |
|         
 | |
|         // 重置播放状态
 | |
|         this.isPlaying = false;
 | |
|         
 | |
|         // 清除全局状态
 | |
|         window.virtualTeacherPlaying = false;
 | |
|         
 | |
|         // 停止说话动画
 | |
|         this.stopSpeaking();
 | |
|         
 | |
|         // 隐藏语音气泡
 | |
|         this.hideSpeechBubble();
 | |
|         
 | |
|         // 重置状态
 | |
|         this.updateStatus('🦉 老师待机中...');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 显示文字气泡
 | |
|      */
 | |
|     showSpeechBubble(text) {
 | |
|         const bubble = document.getElementById('teacher-speech-bubble');
 | |
|         if (bubble) {
 | |
|             bubble.textContent = text;
 | |
|             bubble.classList.add('visible');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 隐藏文字气泡
 | |
|      */
 | |
|     hideSpeechBubble() {
 | |
|         const bubble = document.getElementById('teacher-speech-bubble');
 | |
|         if (bubble) {
 | |
|             bubble.classList.remove('visible');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 开始说话动画
 | |
|      */
 | |
|     startSpeaking() {
 | |
|         this.isSpeaking = true;
 | |
|         this.container.classList.add('speaking');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 停止说话动画
 | |
|      */
 | |
|     stopSpeaking() {
 | |
|         this.isSpeaking = false;
 | |
|         this.container.classList.remove('speaking');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 更新状态显示
 | |
|      */
 | |
|     updateStatus(text) {
 | |
|         const status = document.getElementById('teacher-status');
 | |
|         if (status) {
 | |
|             status.textContent = text;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 眨眼动画
 | |
|      */
 | |
|     blink() {
 | |
|         if (!this.owl || !this.owl.userData) return;
 | |
| 
 | |
|         const userData = this.owl.userData;
 | |
|         userData.leftEyeOuter.scale.y = 0.1;
 | |
|         userData.rightEyeOuter.scale.y = 0.1;
 | |
|         userData.leftEyeInner.scale.y = 0.1;
 | |
|         userData.rightEyeInner.scale.y = 0.1;
 | |
| 
 | |
|         setTimeout(() => {
 | |
|             userData.leftEyeOuter.scale.y = 1;
 | |
|             userData.rightEyeOuter.scale.y = 1;
 | |
|             userData.leftEyeInner.scale.y = 1;
 | |
|             userData.rightEyeInner.scale.y = 1;
 | |
|         }, 200);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 改变表情
 | |
|      */
 | |
|     changeExpression(expression) {
 | |
|         if (!this.owl || !this.owl.userData) return;
 | |
| 
 | |
|         const userData = this.owl.userData;
 | |
|         this.currentExpression = expression;
 | |
| 
 | |
|         switch (expression) {
 | |
|             case "happy":
 | |
|                 userData.leftEyeOuter.scale.y = 0.7;
 | |
|                 userData.rightEyeOuter.scale.y = 0.7;
 | |
|                 userData.beak.scale.set(1.2, 1, 1);
 | |
|                 this.updateStatus("😊 开心模式");
 | |
|                 break;
 | |
|             case "excited":
 | |
|                 userData.leftEyeOuter.scale.set(1.2, 1.2, 1);
 | |
|                 userData.rightEyeOuter.scale.set(1.2, 1.2, 1);
 | |
|                 userData.beak.scale.set(1.3, 1.1, 1);
 | |
|                 this.updateStatus("🤩 兴奋模式");
 | |
|                 break;
 | |
|             case "thinking":
 | |
|                 userData.head.rotation.z = 0.2;
 | |
|                 userData.leftPupil.position.x = -10;
 | |
|                 userData.rightPupil.position.x = 14;
 | |
|                 this.updateStatus("🤔 思考模式");
 | |
|                 break;
 | |
|             case "surprised":
 | |
|                 userData.leftEyeOuter.scale.set(1.3, 1.3, 1);
 | |
|                 userData.rightEyeOuter.scale.set(1.3, 1.3, 1);
 | |
|                 userData.beak.scale.set(0.8, 1.2, 1);
 | |
|                 this.updateStatus("😮 惊讶模式");
 | |
|                 break;
 | |
|             case "encouraging":
 | |
|                 userData.leftEyeOuter.scale.y = 0.8;
 | |
|                 userData.rightEyeOuter.scale.y = 0.8;
 | |
|                 userData.beak.scale.set(1.1, 0.9, 1);
 | |
|                 this.updateStatus("💪 鼓励模式");
 | |
|                 break;
 | |
|             case "proud":
 | |
|                 userData.head.position.y = 18;
 | |
|                 userData.beak.scale.set(1.2, 1.2, 1);
 | |
|                 this.updateStatus("🦉 骄傲模式");
 | |
|                 break;
 | |
|             default: // normal
 | |
|                 userData.leftEyeOuter.scale.set(1, 1, 1);
 | |
|                 userData.rightEyeOuter.scale.set(1, 1, 1);
 | |
|                 userData.leftEyeInner.scale.set(1, 1, 1);
 | |
|                 userData.rightEyeInner.scale.set(1, 1, 1);
 | |
|                 userData.head.rotation.z = 0;
 | |
|                 userData.head.position.y = 15;
 | |
|                 userData.leftPupil.position.x = -12;
 | |
|                 userData.rightPupil.position.x = 12;
 | |
|                 userData.beak.scale.set(1, 0.8, 1);
 | |
|                 this.updateStatus("😐 正常模式");
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 记录正确答案
 | |
|      */
 | |
|     recordCorrect() {
 | |
|         this.gameStats.correct++;
 | |
|         this.gameStats.total++;
 | |
|         this.gameStats.streak++;
 | |
|         
 | |
|         if (this.gameStats.streak > this.gameStats.maxStreak) {
 | |
|             this.gameStats.maxStreak = this.gameStats.streak;
 | |
|         }
 | |
| 
 | |
|         // 根据连续正确次数选择表情和语音
 | |
|         if (this.gameStats.streak >= 5) {
 | |
|             this.changeExpression("excited");
 | |
|             this.speak("太棒了!你已经连续答对" + this.gameStats.streak + "题了!继续保持!");
 | |
|         } else if (this.gameStats.streak >= 3) {
 | |
|             this.changeExpression("happy");
 | |
|             this.speak("很好!连续答对了" + this.gameStats.streak + "题!");
 | |
|         } else {
 | |
|             this.changeExpression("happy");
 | |
|             const messages = [
 | |
|                 "正确!做得很好!",
 | |
|                 "太棒了!你答对了!",
 | |
|                 "很好!继续加油!",
 | |
|                 "答对了!你真聪明!"
 | |
|             ];
 | |
|             this.speak(messages[Math.floor(Math.random() * messages.length)]);
 | |
|         }
 | |
| 
 | |
|         console.log('🦉 老师记录正确答案:', this.gameStats);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 记录错误答案
 | |
|      */
 | |
|     recordIncorrect() {
 | |
|         this.gameStats.incorrect++;
 | |
|         this.gameStats.total++;
 | |
|         this.gameStats.streak = 0; // 重置连续正确次数
 | |
| 
 | |
|         // 根据错误情况选择表情和语音
 | |
|         if (this.gameStats.incorrect <= 2) {
 | |
|             this.changeExpression("encouraging");
 | |
|             const messages = [
 | |
|                 "没关系,再试一次!",
 | |
|                 "不要灰心,继续努力!",
 | |
|                 "错了也没关系,学习就是这样!",
 | |
|                 "加油!你可以的!"
 | |
|             ];
 | |
|             this.speak(messages[Math.floor(Math.random() * messages.length)]);
 | |
|         } else {
 | |
|             this.changeExpression("thinking");
 | |
|             this.speak("让我们重新思考一下这个问题,不要放弃!");
 | |
|         }
 | |
| 
 | |
|         console.log('🦉 老师记录错误答案:', this.gameStats);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 游戏开始
 | |
|      */
 | |
|     gameStart(gameName) {
 | |
|         this.changeExpression("normal");
 | |
|         this.speak("游戏开始!" + gameName + ",准备好了吗?");
 | |
|         console.log('🦉 老师:游戏开始 -', gameName);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 游戏结束
 | |
|      */
 | |
|     gameEnd() {
 | |
|         const accuracy = this.gameStats.total > 0 ? 
 | |
|             Math.round((this.gameStats.correct / this.gameStats.total) * 100) : 0;
 | |
|         
 | |
|         if (accuracy >= 80) {
 | |
|             this.changeExpression("proud");
 | |
|             this.speak("游戏结束!你的正确率是" + accuracy + "%,表现非常优秀!");
 | |
|         } else if (accuracy >= 60) {
 | |
|             this.changeExpression("happy");
 | |
|             this.speak("游戏结束!你的正确率是" + accuracy + "%,还不错!");
 | |
|         } else {
 | |
|             this.changeExpression("encouraging");
 | |
|             this.speak("游戏结束!你的正确率是" + accuracy + "%,继续练习会更好的!");
 | |
|         }
 | |
| 
 | |
|         console.log('🦉 老师:游戏结束,正确率:', accuracy + '%');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 重置游戏统计
 | |
|      */
 | |
|     resetStats() {
 | |
|         this.gameStats = {
 | |
|             correct: 0,
 | |
|             incorrect: 0,
 | |
|             total: 0,
 | |
|             streak: 0,
 | |
|             maxStreak: 0
 | |
|         };
 | |
|         console.log('🦉 老师:重置游戏统计');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 朗读游戏介绍
 | |
|      */
 | |
|     readGameIntroduction(introText) {
 | |
|         if (introText) {
 | |
|             this.changeExpression("normal");
 | |
|             this.speak(introText);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 朗读题目
 | |
|      */
 | |
|     readQuestion(questionText) {
 | |
|         if (questionText) {
 | |
|             this.changeExpression("thinking");
 | |
|             this.speak("题目是:" + questionText);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 销毁虚拟老师
 | |
|      */
 | |
|     destroy() {
 | |
|         if (this.animationId) {
 | |
|             cancelAnimationFrame(this.animationId);
 | |
|         }
 | |
|         
 | |
|         if (this.speechSynthesis.speaking) {
 | |
|             this.speechSynthesis.cancel();
 | |
|         }
 | |
|         
 | |
|         if (this.container && this.container.parentNode) {
 | |
|             this.container.parentNode.removeChild(this.container);
 | |
|         }
 | |
|         
 | |
|         this.isInitialized = false;
 | |
|         console.log('🦉 虚拟老师已销毁');
 | |
|     }
 | |
| }
 | |
| 
 | |
| // 创建全局实例
 | |
| window.virtualTeacher = new VirtualTeacher();
 | |
| 
 | |
| // 导出类
 | |
| if (typeof module !== 'undefined' && module.exports) {
 | |
|     module.exports = VirtualTeacher;
 | |
| }
 |