Add epic animations to Word Storm good/bad responses

🎉 GOOD ANSWER ANIMATIONS:
- Enhanced explosion with color transitions (blue→green→orange→red)
- Screen shake effect for impact feedback
- Floating points popup (+10, +12, etc.) with smooth animation
- Gentle vibration pattern for positive reinforcement

 BAD ANSWER ANIMATIONS:
- Red shake animation for all falling words
- Answer panel flash with red glow effect
- Full screen red overlay flash
- Strong vibration pattern for negative feedback

🎨 TECHNICAL IMPROVEMENTS:
- New CSS keyframes: explode, wrongShake, wrongFlash, screenShake, pointsFloat
- Enhanced correctAnswer() method with screen shake and points popup
- Enhanced wrongAnswer() method with multi-element animations
- Vibration API integration for tactile feedback
- Proper animation cleanup and timing

🎯 UX ENHANCEMENT:
- Much more satisfying and engaging gameplay experience
- Clear visual distinction between success and failure
- Gamification elements that motivate continued play

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-09-20 12:09:44 +08:00
parent e67e40f09b
commit 638c734578
6 changed files with 434 additions and 54 deletions

View File

@ -752,3 +752,53 @@ python3 -m http.server 8000
5. **Use global CSS classes** - Don't reinvent layout, build on existing structure
**Remember: Most bugs are simple syntax errors (especially template literals) or missing module registrations. Check these first!** 🎯
## 🤝 **Collaborative Development Best Practices**
**Critical lesson learned from real debugging sessions:**
### **✅ Always Test Before Committing**
**❌ BAD WORKFLOW:**
1. Write code
2. Immediately commit
3. Discover it doesn't work
4. Debug on committed broken code
**✅ GOOD WORKFLOW:**
1. Write code
2. **TEST THOROUGHLY**
3. If broken → debug cooperatively
4. When working → commit
### **🔍 Cooperative Debugging Method**
**When user reports: "ça marche pas" or "y'a pas de lettres":**
1. **Get specific symptoms** - Don't assume, ask exactly what they see
2. **Add targeted debug logs** - Console.log the exact variables in question
3. **Test together** - Have user run and report console output
4. **Analyze together** - Look at debug output to find root cause
5. **Fix precisely** - Target the exact issue, don't rewrite everything
**Real Example - Letter Discovery Issue:**
```javascript
// ❌ ASSUMPTION: "Letters not working, must rewrite everything"
// ✅ ACTUAL DEBUG:
console.log('🔍 DEBUG this.content.letters:', this.content.letters); // undefined
console.log('🔍 DEBUG this.content.rawContent?.letters:', this.content.rawContent?.letters); // {U: Array(4), V: Array(4), T: Array(4)}
// ✅ PRECISE FIX: Check both locations
const letters = this.content.letters || this.content.rawContent?.letters;
```
### **🎯 Key Principles**
- **Communication > Code** - Clear problem description saves hours
- **Debug logs > Assumptions** - Add console.log to see actual data
- **Test early, test often** - Don't tunnel vision on untested code
- **Pair debugging** - Two brains spot issues faster than one
- **Patience > Speed** - Taking time to understand beats rushing broken fixes
**"C'est mieux quand on prend notre temps en coop plutot que de tunnel vision !"** 🎯

View File

@ -298,6 +298,100 @@ window.ContentModules.WTA1B1 = {
}
},
// === LETTERS DISCOVERY SYSTEM ===
letters: {
"U": [
{
word: "unhappy",
translation: "不开心的",
type: "adjective",
pronunciation: "ʌnhæpi",
example: "The cat looks unhappy."
},
{
word: "umbrella",
translation: "雨伞",
type: "noun",
pronunciation: "ʌmbrɛlə",
example: "I need an umbrella when it rains."
},
{
word: "up",
translation: "向上",
type: "adverb",
pronunciation: "ʌp",
example: "The bird flies up high."
},
{
word: "under",
translation: "在...下面",
type: "preposition",
pronunciation: "ʌndər",
example: "The cat hides under the table."
}
],
"V": [
{
word: "violet",
translation: "紫色的",
type: "adjective",
pronunciation: "vaɪələt",
example: "She has a violet dress."
},
{
word: "van",
translation: "面包车",
type: "noun",
pronunciation: "væn",
example: "The vet drives a white van."
},
{
word: "vet",
translation: "兽医",
type: "noun",
pronunciation: "vɛt",
example: "The vet takes care of pets."
},
{
word: "vest",
translation: "背心",
type: "noun",
pronunciation: "vɛst",
example: "He wears a warm vest."
}
],
"T": [
{
word: "tall",
translation: "高的",
type: "adjective",
pronunciation: "tɔl",
example: "The teacher is very tall."
},
{
word: "turtle",
translation: "海龟",
type: "noun",
pronunciation: "tɜrtəl",
example: "The turtle moves slowly."
},
{
word: "tent",
translation: "帐篷",
type: "noun",
pronunciation: "tɛnt",
example: "We sleep in a tent when camping."
},
{
word: "tiger",
translation: "老虎",
type: "noun",
pronunciation: "taɪgər",
example: "The tiger is a big cat."
}
]
},
vocabulary: {
"unhappy": {
"user_language": "不开心的",

View File

@ -13,7 +13,8 @@ class ContentGameCompatibility {
'chinese-study': 35,
'story-builder': 35,
'story-reader': 40,
'word-storm': 15
'word-storm': 15,
'letter-discovery': 60
};
}
@ -91,12 +92,15 @@ class ContentGameCompatibility {
hasCorrections: this.hasContent(content, 'corrections'),
hasComprehension: this.hasContent(content, 'comprehension'),
hasMatching: this.hasContent(content, 'matching'),
hasLetters: this.hasContent(content, 'letters'),
// Compteurs
vocabularyCount: this.countItems(content, 'vocabulary'),
sentenceCount: this.countItems(content, 'sentences'),
dialogueCount: this.countItems(content, 'dialogues'),
grammarCount: this.countItems(content, 'grammar')
grammarCount: this.countItems(content, 'grammar'),
letterCount: this.countItems(content, 'letters'),
letterWordsCount: this.calculateAverageWordsPerLetter(content)
};
}
@ -133,6 +137,9 @@ class ContentGameCompatibility {
case 'word-storm':
return this.calculateWordStormCompat(capabilities);
case 'letter-discovery':
return this.calculateLetterDiscoveryCompat(capabilities);
default:
return { compatible: true, score: 50, reason: 'Jeu non spécifiquement analysé' };
}
@ -390,6 +397,36 @@ class ContentGameCompatibility {
};
}
calculateLetterDiscoveryCompat(capabilities) {
let score = 0;
const reasons = [];
// Letter Discovery requires predefined letters structure
if (capabilities.hasLetters) {
score += 80;
const letterCount = capabilities.letterCount || 'unknown';
reasons.push(`Structure de lettres prédéfinie (${letterCount} lettres)`);
} else {
return {
compatible: false,
score: 0,
reason: 'Nécessite une structure de lettres prédéfinie (content.letters)'
};
}
// Bonus for well-structured letter content
if (capabilities.letterWordsCount && capabilities.letterWordsCount >= 3) {
score += 20;
reasons.push(`${capabilities.letterWordsCount} mots par lettre en moyenne`);
}
return {
compatible: score >= 60,
score,
reason: score >= 60 ? `Compatible: ${reasons.join(', ')}` : 'Structure de lettres insuffisante'
};
}
// === UTILITAIRES ===
hasContent(content, type) {
@ -458,6 +495,23 @@ class ContentGameCompatibility {
return 0;
}
calculateAverageWordsPerLetter(content) {
const letters = content.letters || content.rawContent?.letters || content.adaptedContent?.letters;
if (!letters || typeof letters !== 'object') return 0;
let totalWords = 0;
let letterCount = 0;
Object.values(letters).forEach(letterWords => {
if (Array.isArray(letterWords)) {
totalWords += letterWords.length;
letterCount++;
}
});
return letterCount > 0 ? Math.round(totalWords / letterCount) : 0;
}
getGameRequirements(gameType) {
const requirements = {
'whack-a-mole': ['5+ mots de vocabulaire OU 3+ phrases', 'Contenu simple et répétitif'],
@ -469,7 +523,8 @@ class ContentGameCompatibility {
'chinese-study': ['Vocabulaire et phrases chinoises', 'Audio recommandé'],
'story-builder': ['Dialogues OU 5+ phrases', 'Vocabulaire varié'],
'story-reader': ['Textes à lire, dialogues recommandés', 'Contenu narratif'],
'word-storm': ['3+ mots de vocabulaire', 'Prononciations recommandées']
'word-storm': ['3+ mots de vocabulaire', 'Prononciations recommandées'],
'letter-discovery': ['Structure de lettres prédéfinie (content.letters)', 'Lettres avec mots associés']
};
return requirements[gameType] || ['Contenu de base'];
@ -484,7 +539,8 @@ class ContentGameCompatibility {
'adventure-reader': 'Aventure narrative nécessitant contenu riche',
'chinese-study': 'Optimisé pour apprentissage du chinois',
'story-builder': 'Construction narrative nécessitant éléments variés',
'story-reader': 'Lecture d\'histoires nécessitant contenu narratif'
'story-reader': 'Lecture d\'histoires nécessitant contenu narratif',
'letter-discovery': 'Apprentissage par lettres nécessitant structure prédéfinie'
};
return reasons[gameType] || 'Compatibilité non évaluée spécifiquement';

View File

@ -199,6 +199,29 @@ class LetterDiscovery {
font-size: 1.1em;
color: #666;
font-style: italic;
margin-bottom: 10px;
}
.word-type {
font-size: 0.9em;
color: #667eea;
background: rgba(102, 126, 234, 0.1);
padding: 4px 12px;
border-radius: 15px;
display: inline-block;
margin-bottom: 15px;
font-weight: 500;
}
.word-example {
font-size: 1em;
color: #555;
font-style: italic;
padding: 10px 15px;
background: rgba(0, 0, 0, 0.05);
border-left: 3px solid #667eea;
border-radius: 0 8px 8px 0;
margin-bottom: 15px;
}
/* Practice Challenge Styles */
@ -369,51 +392,33 @@ class LetterDiscovery {
extractContent() {
logSh('🔍 Letter Discovery - Extracting content...', 'INFO');
// Check if content has letter structure
if (this.content.letters) {
this.letters = Object.keys(this.content.letters);
this.letterWords = this.content.letters;
// Check for letters in content or rawContent
const letters = this.content.letters || this.content.rawContent?.letters;
if (letters && Object.keys(letters).length > 0) {
this.letters = Object.keys(letters).sort();
this.letterWords = letters;
logSh(`📝 Found ${this.letters.length} letters with words`, 'INFO');
} else {
// Fallback: Create letter structure from vocabulary
this.generateLetterStructure();
}
if (this.letters.length === 0) {
throw new Error('No letters found in content');
this.showNoLettersMessage();
return;
}
logSh(`🎯 Letter Discovery ready: ${this.letters.length} letters`, 'INFO');
}
generateLetterStructure() {
logSh('🔧 Generating letter structure from vocabulary...', 'INFO');
const letterMap = {};
if (this.content.vocabulary) {
Object.keys(this.content.vocabulary).forEach(word => {
const firstLetter = word.charAt(0).toUpperCase();
if (!letterMap[firstLetter]) {
letterMap[firstLetter] = [];
}
const wordData = this.content.vocabulary[word];
letterMap[firstLetter].push({
word: word,
translation: typeof wordData === 'string' ? wordData : wordData.translation || wordData.user_language,
pronunciation: wordData.pronunciation || wordData.prononciation,
type: wordData.type,
image: wordData.image,
audioFile: wordData.audioFile
});
});
}
this.letters = Object.keys(letterMap).sort();
this.letterWords = letterMap;
logSh(`📝 Generated ${this.letters.length} letters from vocabulary`, 'INFO');
showNoLettersMessage() {
this.container.innerHTML = `
<div class="game-error">
<div class="error-content">
<h2>🔤 Letter Discovery</h2>
<p> No letter structure found in this content.</p>
<p>This game requires content with a predefined letters system.</p>
<p>Try with content that includes letter-based learning material.</p>
<button class="back-btn" onclick="AppNavigation.navigateTo('games')"> Back to Games</button>
</div>
</div>
`;
}
init() {
@ -560,6 +565,8 @@ class LetterDiscovery {
<div class="word-text">${word.word}</div>
<div class="word-translation">${word.translation}</div>
${word.pronunciation ? `<div class="word-pronunciation">[${word.pronunciation}]</div>` : ''}
${word.type ? `<div class="word-type">${word.type}</div>` : ''}
${word.example ? `<div class="word-example">"${word.example}"</div>` : ''}
<div class="letter-controls">
<button class="discovery-btn" onclick="window.currentLetterGame.nextWord()">
Next Word

View File

@ -844,6 +844,10 @@ class StoryReader {
// Check if sentence has word-by-word data (old format) or needs automatic matching
let wordsHtml;
console.log('🔍 DEBUG: sentence data:', data);
console.log('🔍 DEBUG: data.words exists?', !!data.words);
console.log('🔍 DEBUG: data.words length:', data.words ? data.words.length : 'N/A');
if (data.words && data.words.length > 0) {
// Old format with word-by-word data
wordsHtml = data.words.map(wordData => {

View File

@ -76,7 +76,15 @@ class WordStormGame {
}
.falling-word.exploding {
animation: explode 0.6s ease-out forwards;
animation: explode 0.8s ease-out forwards;
}
.falling-word.wrong-shake {
animation: wrongShake 0.6s ease-in-out forwards;
}
.answer-panel.wrong-flash {
animation: wrongFlash 0.5s ease-in-out;
}
@keyframes wordGlow {
@ -85,9 +93,89 @@ class WordStormGame {
}
@keyframes explode {
0% { transform: translateX(-50%) scale(1); opacity: 1; }
50% { transform: translateX(-50%) scale(1.2); opacity: 0.8; }
100% { transform: translateX(-50%) scale(0.3); opacity: 0; }
0% {
transform: translateX(-50%) scale(1) rotate(0deg);
opacity: 1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(102, 126, 234, 0.4);
}
25% {
transform: translateX(-50%) scale(1.3) rotate(5deg);
opacity: 0.9;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5), 0 0 40px rgba(16, 185, 129, 0.8);
}
50% {
transform: translateX(-50%) scale(1.5) rotate(-3deg);
opacity: 0.7;
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
box-shadow: 0 12px 35px rgba(245, 158, 11, 0.6), 0 0 60px rgba(245, 158, 11, 0.9);
}
75% {
transform: translateX(-50%) scale(0.8) rotate(2deg);
opacity: 0.4;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
100% {
transform: translateX(-50%) scale(0.1) rotate(0deg);
opacity: 0;
}
}
@keyframes wrongShake {
0%, 100% {
transform: translateX(-50%) scale(1);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
10%, 30%, 50%, 70%, 90% {
transform: translateX(-60%) scale(0.95);
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8);
}
20%, 40%, 60%, 80% {
transform: translateX(-40%) scale(0.95);
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.6), 0 0 25px rgba(239, 68, 68, 0.8);
}
}
@keyframes wrongFlash {
0%, 100% {
background: transparent;
box-shadow: none;
}
50% {
background: rgba(239, 68, 68, 0.4);
box-shadow: 0 0 20px rgba(239, 68, 68, 0.6), inset 0 0 20px rgba(239, 68, 68, 0.3);
}
}
@keyframes screenShake {
0%, 100% { transform: translateX(0); }
10% { transform: translateX(-3px) translateY(1px); }
20% { transform: translateX(3px) translateY(-1px); }
30% { transform: translateX(-2px) translateY(2px); }
40% { transform: translateX(2px) translateY(-2px); }
50% { transform: translateX(-1px) translateY(1px); }
60% { transform: translateX(1px) translateY(-1px); }
70% { transform: translateX(-2px) translateY(0px); }
80% { transform: translateX(2px) translateY(1px); }
90% { transform: translateX(-1px) translateY(-1px); }
}
@keyframes pointsFloat {
0% {
transform: translateY(0) scale(1);
opacity: 1;
}
30% {
transform: translateY(-20px) scale(1.3);
opacity: 1;
}
100% {
transform: translateY(-80px) scale(0.5);
opacity: 0;
}
}
@media (max-width: 768px) {
@ -323,14 +411,26 @@ class WordStormGame {
}
correctAnswer(fallingWord) {
// Remove from game
// Remove from game with epic explosion
if (fallingWord.element.parentNode) {
fallingWord.element.classList.add('exploding');
// Add screen shake effect
const gameArea = document.getElementById('game-area');
if (gameArea) {
gameArea.style.animation = 'none';
gameArea.offsetHeight; // Force reflow
gameArea.style.animation = 'screenShake 0.3s ease-in-out';
setTimeout(() => {
gameArea.style.animation = '';
}, 300);
}
setTimeout(() => {
if (fallingWord.element.parentNode) {
fallingWord.element.remove();
}
}, 600);
}, 800);
}
// Remove from tracking
@ -342,10 +442,18 @@ class WordStormGame {
this.score += points;
this.onScoreUpdate(this.score);
// Update display
// Update display with animation
document.getElementById('score').textContent = this.score;
document.getElementById('combo').textContent = this.combo;
// Add points popup animation
this.showPointsPopup(points, fallingWord.element);
// Vibration feedback (if supported)
if (navigator.vibrate) {
navigator.vibrate([50, 30, 50]);
}
// Level up check
if (this.score > 0 && this.score % 100 === 0) {
this.levelUp();
@ -356,13 +464,74 @@ class WordStormGame {
this.combo = 0;
document.getElementById('combo').textContent = this.combo;
// Flash effect
// Enhanced wrong answer animation
const answerPanel = document.getElementById('answer-panel');
if (answerPanel) {
answerPanel.style.background = 'rgba(239, 68, 68, 0.3)';
answerPanel.classList.add('wrong-flash');
setTimeout(() => {
answerPanel.style.background = '';
}, 300);
answerPanel.classList.remove('wrong-flash');
}, 500);
}
// Shake all falling words to show disappointment
this.fallingWords.forEach(fw => {
if (fw.element.parentNode && !fw.element.classList.contains('exploding')) {
fw.element.classList.add('wrong-shake');
setTimeout(() => {
fw.element.classList.remove('wrong-shake');
}, 600);
}
});
// Screen flash red
const gameArea = document.getElementById('game-area');
if (gameArea) {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(239, 68, 68, 0.3);
pointer-events: none;
animation: wrongFlash 0.4s ease-in-out;
z-index: 100;
`;
gameArea.appendChild(overlay);
setTimeout(() => {
if (overlay.parentNode) overlay.remove();
}, 400);
}
// Wrong answer vibration (stronger/longer)
if (navigator.vibrate) {
navigator.vibrate([200, 100, 200, 100, 200]);
}
}
showPointsPopup(points, wordElement) {
const popup = document.createElement('div');
popup.textContent = `+${points}`;
popup.style.cssText = `
position: absolute;
left: ${wordElement.style.left};
top: ${wordElement.offsetTop}px;
font-size: 2rem;
font-weight: bold;
color: #10b981;
pointer-events: none;
z-index: 1000;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
animation: pointsFloat 1.5s ease-out forwards;
`;
const gameArea = document.getElementById('game-area');
if (gameArea) {
gameArea.appendChild(popup);
setTimeout(() => {
if (popup.parentNode) popup.remove();
}, 1500);
}
}