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;
|
|
}
|