移除已追踪的视频资源,按.gitignore排除
This commit is contained in:
@@ -1,854 +0,0 @@
|
||||
/**
|
||||
* 虚拟猫头鹰老师模块
|
||||
* 提供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;
|
||||
}
|
||||
Reference in New Issue
Block a user