import Module from '../core/Module.js'; /** * WizardSpellCaster - Advanced RPG-style spell casting game * Players construct sentences to cast magical spells and defeat enemies */ class WizardSpellCaster extends Module { constructor(name, dependencies, config = {}) { super(name, ['eventBus']); // Validate dependencies if (!dependencies.eventBus || !dependencies.content) { throw new Error('WizardSpellCaster requires eventBus and content dependencies'); } this._eventBus = dependencies.eventBus; this._content = dependencies.content; this._config = { container: null, enemyAttackInterval: { min: 8000, max: 15000 }, enemyDamage: { min: 12, max: 20 }, maxPlayerHP: 100, maxEnemyHP: 100, ...config }; // Game state this._score = 0; this._enemyHP = this._config.maxEnemyHP; this._playerHP = this._config.maxPlayerHP; this._gameStartTime = null; // Spell system this._spells = { short: [], medium: [], long: [] }; this._currentSpells = []; this._selectedSpell = null; this._selectedWords = []; this._spellStartTime = null; this._averageSpellTime = 0; this._spellCount = 0; // Enemy attack system this._enemyAttackTimer = null; this._nextEnemyAttack = 0; Object.seal(this); } /** * Get game metadata * @returns {Object} Game metadata */ static getMetadata() { return { name: 'Wizard Spell Caster', description: 'Advanced RPG spell casting game with sentence construction and magical combat', difficulty: 'advanced', category: 'rpg', estimatedTime: 10, // minutes skills: ['grammar', 'sentences', 'vocabulary', 'strategy', 'speed'] }; } /** * Calculate compatibility score with content * @param {Object} content - Content to check compatibility with * @returns {Object} Compatibility score and details */ static getCompatibilityScore(content) { const sentences = content?.sentences || []; const storyChapters = content?.story?.chapters || []; const dialogues = content?.dialogues || []; let totalSentences = sentences.length; // Count sentences from story chapters storyChapters.forEach(chapter => { if (chapter.sentences) { totalSentences += chapter.sentences.length; } }); // Count sentences from dialogues dialogues.forEach(dialogue => { if (dialogue.conversation) { totalSentences += dialogue.conversation.length; } }); // If we have enough sentences, use them if (totalSentences >= 9) { const score = Math.min(totalSentences / 30, 1); return { score, reason: `${totalSentences} sentences available for spell construction`, requirements: ['sentences', 'story', 'dialogues'], minSentences: 9, optimalSentences: 30, details: `Can create engaging spell combat with ${totalSentences} sentences` }; } // Fallback: Check vocabulary for creating basic spell phrases let vocabCount = 0; let hasVocabulary = false; if (Array.isArray(content?.vocabulary)) { vocabCount = content.vocabulary.length; hasVocabulary = vocabCount > 0; } else if (content?.vocabulary && typeof content.vocabulary === 'object') { vocabCount = Object.keys(content.vocabulary).length; hasVocabulary = vocabCount > 0; } if (hasVocabulary && vocabCount >= 15) { // Can create basic spell phrases from vocabulary let score = 0.3; // Base score for vocabulary-based spells if (vocabCount >= 20) score += 0.1; if (vocabCount >= 30) score += 0.1; if (vocabCount >= 40) score += 0.1; return { score: Math.min(score, 0.6), // Cap at 60% for vocabulary-only content reason: `${vocabCount} vocabulary words available for spell creation`, requirements: ['vocabulary'], minWords: 15, optimalWords: 40, details: `Can create basic spell combat using ${vocabCount} vocabulary words` }; } return { score: 0, reason: `Insufficient content (${totalSentences} sentences, ${vocabCount} vocabulary words)`, requirements: ['sentences', 'story', 'dialogues', 'vocabulary'], minSentences: 9, minWords: 15, details: 'Wizard Spell Caster needs at least 9 sentences or 15 vocabulary words' }; } async init() { this._validateNotDestroyed(); try { // Validate container if (!this._config.container) { throw new Error('Game container is required'); } // Extract and validate spells this._extractSpells(); if (this._getTotalSpellCount() < 9) { throw new Error(`Insufficient spells: need 9, got ${this._getTotalSpellCount()}`); } // Set up event listeners this._eventBus.on('game:pause', this._handlePause.bind(this), this.name); this._eventBus.on('game:resume', this._handleResume.bind(this), this.name); // Inject CSS this._injectCSS(); // Initialize game interface this._createGameInterface(); this._setupEventListeners(); this._generateNewSpells(); this._startEnemyAttackSystem(); // Start the game this._gameStartTime = Date.now(); // Emit game ready event this._eventBus.emit('game:ready', { gameId: 'wizard-spell-caster', instanceId: this.name, spells: { short: this._spells.short.length, medium: this._spells.medium.length, long: this._spells.long.length } }, this.name); this._setInitialized(); } catch (error) { this._showError(error.message); throw error; } } async destroy() { this._validateNotDestroyed(); // Clear timers if (this._enemyAttackTimer) { clearTimeout(this._enemyAttackTimer); this._enemyAttackTimer = null; } // Remove CSS this._removeCSS(); // Clean up event listeners if (this._config.container) { this._config.container.innerHTML = ''; } // Emit game end event this._eventBus.emit('game:ended', { gameId: 'wizard-spell-caster', instanceId: this.name, score: this._score, playerHP: this._playerHP, enemyHP: this._enemyHP, spellsCast: this._spellCount, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }, this.name); this._setDestroyed(); } /** * Get current game state * @returns {Object} Current game state */ getGameState() { this._validateInitialized(); return { score: this._score, playerHP: this._playerHP, enemyHP: this._enemyHP, maxPlayerHP: this._config.maxPlayerHP, maxEnemyHP: this._config.maxEnemyHP, spellsCast: this._spellCount, averageSpellTime: this._averageSpellTime, isComplete: this._playerHP <= 0 || this._enemyHP <= 0, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }; } // Private methods _extractSpells() { this._spells = { short: [], medium: [], long: [] }; // Extract from sentences if (this._content.sentences) { this._content.sentences.forEach(sentence => { const originalText = sentence.english || sentence.original_language; if (originalText) { this._processSentence({ original: originalText, translation: sentence.chinese || sentence.french || sentence.user_language || sentence.translation, words: this._extractWordsFromSentence(originalText) }); } }); } // Extract from story chapters if (this._content.story?.chapters) { this._content.story.chapters.forEach(chapter => { if (chapter.sentences) { chapter.sentences.forEach(sentence => { if (sentence.original) { this._processSentence({ original: sentence.original, translation: sentence.translation, words: sentence.words || this._extractWordsFromSentence(sentence.original) }); } }); } }); } // Extract from dialogues if (this._content.dialogues) { this._content.dialogues.forEach(dialogue => { if (dialogue.conversation) { dialogue.conversation.forEach(line => { if (line.english && line.chinese) { this._processSentence({ original: line.english, translation: line.chinese, words: this._extractWordsFromSentence(line.english) }); } }); } }); } } _processSentence(sentenceData) { if (!sentenceData.original || !sentenceData.translation) return; const wordCount = sentenceData.words.length; const spellData = { english: sentenceData.original, translation: sentenceData.translation, words: sentenceData.words, damage: this._calculateDamage(wordCount), castTime: this._calculateCastTime(wordCount) }; if (wordCount <= 4) { this._spells.short.push(spellData); } else if (wordCount <= 6) { this._spells.medium.push(spellData); } else { this._spells.long.push(spellData); } } _extractWordsFromSentence(sentence) { // Validate input sentence if (!sentence || typeof sentence !== 'string') { console.warn('WizardSpellCaster: Invalid sentence provided to _extractWordsFromSentence:', sentence); return []; } // Simple word extraction with punctuation handling const words = sentence.split(/\s+/).map(word => { return { word: word.replace(/[.!?,;:]/g, ''), translation: word.replace(/[.!?,;:]/g, ''), type: 'word' }; }).filter(wordData => wordData.word.length > 0); // Add punctuation as separate elements const punctuation = sentence.match(/[.!?,;:]/g) || []; punctuation.forEach((punct, index) => { words.push({ word: punct, translation: punct, type: 'punctuation', uniqueId: `punct_${index}_${Date.now()}_${Math.random()}` }); }); return words; } _calculateDamage(wordCount) { if (wordCount <= 3) return Math.floor(Math.random() * 10) + 15; // 15-25 if (wordCount <= 5) return Math.floor(Math.random() * 15) + 30; // 30-45 if (wordCount <= 7) return Math.floor(Math.random() * 20) + 50; // 50-70 return Math.floor(Math.random() * 30) + 70; // 70-100 } _calculateCastTime(wordCount) { if (wordCount <= 4) return 1000; // 1 second if (wordCount <= 6) return 2000; // 2 seconds return 3000; // 3 seconds } _getTotalSpellCount() { return this._spells.short.length + this._spells.medium.length + this._spells.long.length; } _injectCSS() { const cssId = `wizard-spell-caster-styles-${this.name}`; if (document.getElementById(cssId)) return; const style = document.createElement('style'); style.id = cssId; style.textContent = ` .wizard-game-wrapper { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); min-height: 100vh; color: white; font-family: 'Fantasy', serif; position: relative; overflow: hidden; } .wizard-hud { display: flex; justify-content: space-between; padding: 15px; background: rgba(0,0,0,0.3); border-bottom: 2px solid #ffd700; } .wizard-stats { display: flex; gap: 20px; align-items: center; } .health-bar { width: 150px; height: 20px; background: rgba(255,255,255,0.2); border-radius: 10px; overflow: hidden; border: 2px solid #ffd700; } .health-fill { height: 100%; background: linear-gradient(90deg, #ff4757, #ff6b7a); transition: width 0.3s ease; } .battle-area { display: flex; height: 60vh; padding: 20px; } .wizard-side { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; } .enemy-side { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; } .wizard-character { width: 120px; height: 120px; background: linear-gradient(45deg, #6c5ce7, #a29bfe); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 48px; margin-bottom: 20px; animation: float 3s ease-in-out infinite; box-shadow: 0 0 30px rgba(108, 92, 231, 0.6); } .enemy-character { width: 150px; height: 150px; background: linear-gradient(45deg, #ff4757, #ff6b7a); border-radius: 20px; display: flex; align-items: center; justify-content: center; font-size: 64px; margin-bottom: 20px; animation: enemyPulse 2s ease-in-out infinite; box-shadow: 0 0 40px rgba(255, 71, 87, 0.6); } @keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } } @keyframes enemyPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } } .spell-casting-area { background: rgba(0,0,0,0.4); border: 2px solid #ffd700; border-radius: 15px; padding: 20px; margin: 20px; } .spell-selection { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 20px; } .spell-card { background: linear-gradient(135deg, #2c2c54, #40407a); border: 2px solid #ffd700; border-radius: 10px; padding: 15px; cursor: pointer; transition: all 0.3s ease; text-align: center; } .spell-card:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(255, 215, 0, 0.3); border-color: #fff; } .spell-card.selected { background: linear-gradient(135deg, #ffd700, #ffed4e); color: #000; transform: scale(1.05); animation: spellCharging 0.5s ease-in-out infinite alternate; } .spell-type { font-size: 12px; color: #ffd700; font-weight: bold; margin-bottom: 5px; } .spell-damage { font-size: 14px; color: #ff6b7a; font-weight: bold; } .sentence-builder { background: rgba(255,255,255,0.1); border-radius: 10px; padding: 15px; margin-bottom: 20px; min-height: 80px; border: 2px dashed #ffd700; } .word-bank { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } .word-tile { background: linear-gradient(135deg, #5f27cd, #8854d0); color: white; padding: 8px 15px; border-radius: 20px; cursor: grab; user-select: none; transition: all 0.3s ease; border: 2px solid transparent; } .word-tile:hover { transform: scale(1.1); box-shadow: 0 5px 15px rgba(95, 39, 205, 0.4); } .word-tile.selected { background: linear-gradient(135deg, #ffd700, #ffed4e); color: #000; border-color: #fff; } .word-tile:active { cursor: grabbing; } .cast-button { background: linear-gradient(135deg, #ff6b7a, #ff4757); border: none; color: white; padding: 15px 30px; border-radius: 25px; font-size: 18px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 5px 15px rgba(255, 71, 87, 0.3); width: 100%; } .cast-button:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(255, 71, 87, 0.5); } .cast-button:disabled { background: #666; cursor: not-allowed; transform: none; box-shadow: none; } .exit-wizard-btn { padding: 8px 15px; background: rgba(255, 255, 255, 0.1); border: 2px solid rgba(255, 255, 255, 0.3); color: white; border-radius: 8px; cursor: pointer; font-size: 0.9rem; transition: all 0.3s ease; } .exit-wizard-btn:hover { background: rgba(255, 255, 255, 0.2); transform: translateY(-2px); } .damage-number { position: absolute; font-size: 36px; font-weight: bold; color: #ff4757; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); pointer-events: none; animation: damageFloat 1.5s ease-out forwards; } @keyframes damageFloat { 0% { opacity: 1; transform: translateY(0) scale(1); } 100% { opacity: 0; transform: translateY(-100px) scale(1.5); } } .spell-effect { position: absolute; width: 100px; height: 100px; border-radius: 50%; pointer-events: none; animation: spellBlast 0.8s ease-out forwards; } .fire-effect { background: radial-gradient(circle, #ff6b7a, #ff4757, #ff3742, transparent); filter: drop-shadow(0 0 20px #ff4757); animation: spellBlast 0.8s ease-out forwards, fireGlow 0.8s ease-out; } .lightning-effect { background: radial-gradient(circle, #ffd700, #ffed4e, #fff200, transparent); filter: drop-shadow(0 0 25px #ffd700); animation: spellBlast 0.8s ease-out forwards, lightningPulse 0.8s ease-out; } .meteor-effect { background: radial-gradient(circle, #a29bfe, #6c5ce7, #5f3dc4, transparent); filter: drop-shadow(0 0 30px #6c5ce7); animation: spellBlast 0.8s ease-out forwards, meteorImpact 0.8s ease-out; } @keyframes spellBlast { 0% { transform: scale(0); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.8; } 100% { transform: scale(3); opacity: 0; } } @keyframes fireGlow { 0%, 100% { filter: drop-shadow(0 0 20px #ff4757) hue-rotate(0deg); } 50% { filter: drop-shadow(0 0 40px #ff4757) hue-rotate(30deg); } } @keyframes lightningPulse { 0%, 100% { filter: drop-shadow(0 0 25px #ffd700) brightness(1); } 50% { filter: drop-shadow(0 0 50px #ffd700) brightness(2); } } @keyframes meteorImpact { 0% { filter: drop-shadow(0 0 30px #6c5ce7) contrast(1); } 30% { filter: drop-shadow(0 0 60px #6c5ce7) contrast(1.5); } 100% { filter: drop-shadow(0 0 30px #6c5ce7) contrast(1); } } @keyframes spellCharging { 0% { box-shadow: 0 0 20px rgba(255, 215, 0, 0.5); transform: scale(1.05); } 100% { box-shadow: 0 0 40px rgba(255, 215, 0, 0.8); transform: scale(1.07); } } .mini-enemy { position: absolute; width: 60px; height: 60px; background: linear-gradient(45deg, #ff9ff3, #f368e0); border-radius: 50%; font-size: 30px; display: flex; align-items: center; justify-content: center; animation: miniEnemyFloat 3s ease-in-out infinite; z-index: 100; } @keyframes miniEnemyFloat { 0%, 100% { transform: translateY(0px) rotate(0deg); } 50% { transform: translateY(-15px) rotate(180deg); } } .magic-quirk { position: fixed; width: 200px; height: 200px; border-radius: 50%; background: conic-gradient(from 0deg, #ff0080, #0080ff, #ff0080); animation: magicQuirk 2s ease-in-out; z-index: 1000; pointer-events: none; } @keyframes magicQuirk { 0% { transform: translate(-50%, -50%) scale(0) rotate(0deg); opacity: 1; } 50% { transform: translate(-50%, -50%) scale(1.5) rotate(180deg); opacity: 0.8; } 100% { transform: translate(-50%, -50%) scale(0) rotate(360deg); opacity: 0; } } .flying-bird { position: fixed; font-size: 48px; z-index: 500; pointer-events: none; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; } .bird-path-1 { animation: flyPath1 8s linear infinite; } .bird-path-2 { animation: flyPath2 6s linear infinite; } .bird-path-3 { animation: flyPath3 10s linear infinite; } @keyframes flyPath1 { 0% { left: -100px; top: 20vh; transform: rotate(0deg) scale(1); } 25% { left: 30vw; top: 5vh; transform: rotate(180deg) scale(1.5); } 50% { left: 70vw; top: 40vh; transform: rotate(-90deg) scale(0.5); } 75% { left: 100vw; top: 15vh; transform: rotate(270deg) scale(2); } 100% { left: -100px; top: 20vh; transform: rotate(360deg) scale(1); } } @keyframes flyPath2 { 0% { left: 50vw; top: -80px; transform: rotate(0deg) scale(0.5); } 33% { left: 80vw; top: 20vh; transform: rotate(-225deg) scale(2.5); } 66% { left: 20vw; top: 50vh; transform: rotate(315deg) scale(0.3); } 100% { left: 50vw; top: -80px; transform: rotate(-630deg) scale(0.5); } } @keyframes flyPath3 { 0% { left: 120vw; top: 10vh; transform: rotate(0deg) scale(1); } 20% { left: 75vw; top: 70vh; transform: rotate(-360deg) scale(4); } 40% { left: 25vw; top: 20vh; transform: rotate(540deg) scale(0.2); } 60% { left: 85vw; top: 90vh; transform: rotate(-720deg) scale(3.5); } 80% { left: 10vw; top: 5vh; transform: rotate(900deg) scale(0.4); } 100% { left: 120vw; top: 10vh; transform: rotate(1800deg) scale(1); } } .screen-shake { animation: screenShake 0.5s ease-in-out; } @keyframes screenShake { 0%, 100% { transform: translateX(0); } 10% { transform: translateX(-10px); } 20% { transform: translateX(10px); } 30% { transform: translateX(-10px); } 40% { transform: translateX(10px); } 50% { transform: translateX(-10px); } 60% { transform: translateX(10px); } 70% { transform: translateX(-10px); } 80% { transform: translateX(10px); } 90% { transform: translateX(-10px); } } .enemy-attack-warning { position: absolute; top: -30px; left: 50%; transform: translateX(-50%); background: #ff4757; color: white; padding: 5px 15px; border-radius: 15px; font-size: 14px; font-weight: bold; animation: warningPulse 1s ease-in-out infinite; z-index: 100; } @keyframes warningPulse { 0%, 100% { opacity: 1; transform: translateX(-50%) scale(1); } 50% { opacity: 0.6; transform: translateX(-50%) scale(1.1); } } .enemy-attack-effect { position: absolute; width: 150px; height: 150px; border-radius: 50%; background: radial-gradient(circle, #ff4757, transparent); animation: enemyAttackBlast 1s ease-out; pointer-events: none; z-index: 200; } @keyframes enemyAttackBlast { 0% { transform: scale(0); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.8; } 100% { transform: scale(3); opacity: 0; } } .enemy-charging { animation: enemyCharging 2s ease-in-out; } @keyframes enemyCharging { 0%, 100% { background: linear-gradient(45deg, #ff4757, #ff6b7a); transform: scale(1); } 50% { background: linear-gradient(45deg, #ff0000, #ff3333); transform: scale(1.1); box-shadow: 0 0 60px rgba(255, 0, 0, 0.8); } } .victory-screen, .defeat-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 1000; } .result-title { font-size: 48px; margin-bottom: 20px; text-shadow: 2px 2px 4px rgba(0,0,0,0.8); } .victory-screen .result-title { color: #2ed573; } .defeat-screen .result-title { color: #ff4757; } .game-result-btn { margin: 10px; padding: 15px 30px; border: 2px solid white; border-radius: 10px; background: transparent; color: white; font-size: 18px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .game-result-btn.victory { border-color: #2ed573; color: #2ed573; } .game-result-btn.victory:hover { background: #2ed573; color: white; } .game-result-btn.defeat { border-color: #ff4757; color: #ff4757; } .game-result-btn.defeat:hover { background: #ff4757; color: white; } .fail-message { position: fixed; top: 30%; left: 50%; transform: translateX(-50%); background: rgba(255, 71, 87, 0.9); color: white; padding: 20px 30px; border-radius: 15px; font-size: 24px; font-weight: bold; z-index: 1000; animation: failMessagePop 2s ease-out; text-align: center; border: 3px solid #ffd700; } @keyframes failMessagePop { 0% { transform: translateX(-50%) scale(0); opacity: 0; } 20% { transform: translateX(-50%) scale(1.2); opacity: 1; } 80% { transform: translateX(-50%) scale(1); opacity: 1; } 100% { transform: translateX(-50%) scale(0.8); opacity: 0; } } .game-error { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; text-align: center; padding: 40px; background: linear-gradient(135deg, #f87171 0%, #ef4444 100%); color: white; } .game-error h3 { font-size: 2rem; margin-bottom: 20px; } .back-btn { padding: 12px 25px; background: white; color: #ef4444; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; margin-top: 20px; } .back-btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(255, 255, 255, 0.3); } @media (max-width: 768px) { .wizard-game-wrapper { padding: 10px; } .battle-area { height: 50vh; padding: 10px; } .wizard-character, .enemy-character { width: 80px; height: 80px; font-size: 32px; } .spell-selection { grid-template-columns: 1fr; gap: 10px; } .word-bank { gap: 5px; } .word-tile { padding: 6px 12px; font-size: 0.9rem; } } `; document.head.appendChild(style); } _removeCSS() { const cssId = `wizard-spell-caster-styles-${this.name}`; const existingStyle = document.getElementById(cssId); if (existingStyle) { existingStyle.remove(); } } _createGameInterface() { this._config.container.innerHTML = `
Wizard HP
Score: 0
Enemy HP
🧙‍♂ïļ
Wizard Master
ðŸ‘đ
Grammar Demon
Form your spell incantation:
`; } _setupEventListeners() { // Cast button document.getElementById('cast-button').addEventListener('click', () => this._castSpell()); // Exit button const exitButton = document.getElementById('exit-wizard'); if (exitButton) { exitButton.addEventListener('click', () => { this._eventBus.emit('game:exit-request', { instanceId: this.name }, this.name); }); } } _generateNewSpells() { this._currentSpells = []; // Get one spell of each type if (this._spells.short.length > 0) { this._currentSpells.push({ ...this._spells.short[Math.floor(Math.random() * this._spells.short.length)], type: 'short', name: 'Fireball', icon: 'ðŸ”Ĩ' }); } if (this._spells.medium.length > 0) { this._currentSpells.push({ ...this._spells.medium[Math.floor(Math.random() * this._spells.medium.length)], type: 'medium', name: 'Lightning', icon: '⚡' }); } if (this._spells.long.length > 0) { this._currentSpells.push({ ...this._spells.long[Math.floor(Math.random() * this._spells.long.length)], type: 'long', name: 'Meteor', icon: '☄ïļ' }); } this._renderSpellCards(); this._selectedSpell = null; this._selectedWords = []; this._updateWordBank(); this._updateSentenceBuilder(); } _renderSpellCards() { const container = document.getElementById('spell-selection'); container.innerHTML = this._currentSpells.map((spell, index) => `
${spell.icon} ${spell.name}
${spell.translation}
${spell.damage} damage
`).join(''); // Add click listeners container.querySelectorAll('.spell-card').forEach(card => { card.addEventListener('click', (e) => { const spellIndex = parseInt(e.currentTarget.dataset.spellIndex); this._selectSpell(spellIndex); }); }); } _selectSpell(index) { // Remove previous selection document.querySelectorAll('.spell-card').forEach(card => card.classList.remove('selected')); // Select new spell this._selectedSpell = this._currentSpells[index]; document.querySelector(`[data-spell-index="${index}"]`).classList.add('selected'); // Start timing for speed bonus this._spellStartTime = Date.now(); // Reset word selection this._selectedWords = []; this._updateWordBank(); this._updateSentenceBuilder(); } _updateWordBank() { const container = document.getElementById('word-bank'); if (!this._selectedSpell) { container.innerHTML = '
Select a spell first
'; return; } // Get words and add punctuation const words = [...this._selectedSpell.words]; // Shuffle the words including punctuation const shuffledWords = [...words].sort(() => Math.random() - 0.5); container.innerHTML = shuffledWords.map((wordData, index) => { const uniqueId = wordData.uniqueId || `word_${index}_${wordData.word}`; return `
${wordData.word}
`; }).join(''); // Add click listeners container.querySelectorAll('.word-tile').forEach(tile => { tile.addEventListener('click', (e) => { const word = e.currentTarget.dataset.word; const uniqueId = e.currentTarget.dataset.uniqueId; this._toggleWord(word, e.currentTarget, uniqueId); }); }); } _toggleWord(word, element, uniqueId) { const wordIndex = this._selectedWords.findIndex(selectedWord => selectedWord.uniqueId === uniqueId ); if (wordIndex > -1) { // Remove word this._selectedWords.splice(wordIndex, 1); element.classList.remove('selected'); } else { // Add word with unique ID this._selectedWords.push({ word: word, uniqueId: uniqueId }); element.classList.add('selected'); } this._updateSentenceBuilder(); this._updateCastButton(); } _updateSentenceBuilder() { const container = document.getElementById('current-sentence'); const sentence = this._buildSentenceFromWords(this._selectedWords); container.textContent = sentence; } _buildSentenceFromWords(words) { let sentence = ''; for (let i = 0; i < words.length; i++) { const wordText = typeof words[i] === 'string' ? words[i] : words[i].word; const isPunctuation = ['.', '!', '?', ',', ';', ':'].includes(wordText); if (i === 0) { sentence = wordText; } else if (isPunctuation) { sentence += wordText; // No space before punctuation } else { sentence += ' ' + wordText; // Space before regular words } } return sentence; } _updateCastButton() { const button = document.getElementById('cast-button'); button.disabled = false; if (this._selectedSpell) { button.textContent = `ðŸ”Ĩ CAST ${this._selectedSpell.name.toUpperCase()} ðŸ”Ĩ`; } else { button.textContent = 'ðŸ”Ĩ CAST SPELL ðŸ”Ĩ'; } } _castSpell() { if (!this._selectedSpell) { this._showFailEffect('noSpell'); return; } // Check if spell is correctly formed const expectedSentence = this._selectedSpell.english; const playerSentence = this._buildSentenceFromWords(this._selectedWords); const isCorrect = playerSentence === expectedSentence; if (isCorrect) { // Successful cast! this._showCastingEffect(this._selectedSpell.type); setTimeout(() => { this._showSpellEffect(this._selectedSpell.type); }, 500); // Deal damage this._enemyHP = Math.max(0, this._enemyHP - this._selectedSpell.damage); this._updateEnemyHealth(); this._showDamageNumber(this._selectedSpell.damage); // Update score with bonuses const wordCount = this._selectedWords.length; let scoreMultiplier = 10; if (wordCount >= 7) scoreMultiplier = 20; else if (wordCount >= 5) scoreMultiplier = 15; // Speed bonus let speedBonus = 0; if (this._spellStartTime) { const spellTime = (Date.now() - this._spellStartTime) / 1000; this._spellCount++; this._averageSpellTime = ((this._averageSpellTime * (this._spellCount - 1)) + spellTime) / this._spellCount; if (spellTime < 10) speedBonus = Math.floor((10 - spellTime) * 50); if (spellTime < 5) speedBonus += 300; if (spellTime < 3) speedBonus += 500; } this._score += (this._selectedSpell.damage * scoreMultiplier) + speedBonus; document.getElementById('current-score').textContent = this._score; // Emit spell cast event this._eventBus.emit('wizard-spell-caster:spell-cast', { gameId: 'wizard-spell-caster', instanceId: this.name, spell: this._selectedSpell, damage: this._selectedSpell.damage, score: this._score, speedBonus }, this.name); // Check win condition if (this._enemyHP <= 0) { this._handleVictory(); return; } // Generate new spells for next round setTimeout(() => { this._generateNewSpells(); this._spellStartTime = Date.now(); }, 1000); } else { // Spell failed! this._showFailEffect(); } } _showSpellEffect(type) { const enemyChar = document.querySelector('.enemy-character'); const rect = enemyChar.getBoundingClientRect(); // Main spell effect const effect = document.createElement('div'); effect.className = `spell-effect ${type}-effect`; effect.style.position = 'fixed'; effect.style.left = rect.left + rect.width/2 - 50 + 'px'; effect.style.top = rect.top + rect.height/2 - 50 + 'px'; document.body.appendChild(effect); // Enhanced effects based on spell type this._createSpellParticles(type, rect); this._triggerSpellAnimation(type, enemyChar); setTimeout(() => { effect.remove(); }, 800); } _createSpellParticles(type, enemyRect) { const particleCount = type === 'meteor' ? 15 : type === 'lightning' ? 12 : 8; for (let i = 0; i < particleCount; i++) { const particle = document.createElement('div'); particle.className = `spell-particle ${type}-particle`; const offsetX = (Math.random() - 0.5) * 200; const offsetY = (Math.random() - 0.5) * 200; particle.style.position = 'fixed'; particle.style.left = enemyRect.left + enemyRect.width/2 + offsetX + 'px'; particle.style.top = enemyRect.top + enemyRect.height/2 + offsetY + 'px'; particle.style.width = '6px'; particle.style.height = '6px'; particle.style.borderRadius = '50%'; particle.style.pointerEvents = 'none'; particle.style.zIndex = '1000'; if (type === 'fire') { particle.style.background = 'radial-gradient(circle, #ff6b7a, #ff4757)'; particle.style.boxShadow = '0 0 10px #ff4757'; } else if (type === 'lightning') { particle.style.background = 'radial-gradient(circle, #ffd700, #ffed4e)'; particle.style.boxShadow = '0 0 15px #ffd700'; } else if (type === 'meteor') { particle.style.background = 'radial-gradient(circle, #a29bfe, #6c5ce7)'; particle.style.boxShadow = '0 0 20px #6c5ce7'; } document.body.appendChild(particle); setTimeout(() => { particle.remove(); }, 1500); } } _triggerSpellAnimation(type, enemyChar) { if (type === 'meteor') { document.body.classList.add('screen-shake'); setTimeout(() => document.body.classList.remove('screen-shake'), 500); } // Enemy hit reaction enemyChar.style.transform = 'scale(1.1)'; enemyChar.style.filter = type === 'fire' ? 'hue-rotate(30deg)' : type === 'lightning' ? 'brightness(1.5)' : 'contrast(1.3)'; setTimeout(() => { enemyChar.style.transform = ''; enemyChar.style.filter = ''; }, 300); } _showCastingEffect(spellType) { const wizardChar = document.querySelector('.wizard-character'); // Wizard glow effect wizardChar.style.filter = 'drop-shadow(0 0 20px #ffd700)'; wizardChar.style.transform = 'scale(1.05)'; setTimeout(() => { wizardChar.style.filter = ''; wizardChar.style.transform = ''; }, 600); } _showDamageNumber(damage) { const damageEl = document.createElement('div'); damageEl.className = 'damage-number'; damageEl.textContent = `-${damage}`; const enemyChar = document.querySelector('.enemy-character'); const rect = enemyChar.getBoundingClientRect(); damageEl.style.position = 'fixed'; damageEl.style.left = rect.left + rect.width/2 + 'px'; damageEl.style.top = rect.top + 'px'; document.body.appendChild(damageEl); setTimeout(() => { damageEl.remove(); }, 1500); } _showFailEffect(type = 'random') { const effects = ['spawnMinion', 'loseHP', 'magicQuirk', 'flyingBirds']; const selectedEffect = type === 'random' ? effects[Math.floor(Math.random() * effects.length)] : type; this._showFailMessage(); switch(selectedEffect) { case 'spawnMinion': this._spawnMiniEnemy(); break; case 'loseHP': this._wizardTakesDamage(); break; case 'magicQuirk': this._triggerMagicQuirk(); break; case 'flyingBirds': this._summonFlyingBirds(); break; case 'noSpell': this._showFailMessage('Select a spell first! 🊄'); break; } } _showFailMessage(customMessage = null) { const messages = [ "Spell backfired! ðŸ’Ĩ", "Magic went wrong! 🌀", "Oops! Wrong incantation! 😅", "The magic gods are not pleased! ⚡", "Your spell turned into chaos! 🎭", "Magic malfunction detected! 🔧" ]; const message = customMessage || messages[Math.floor(Math.random() * messages.length)]; const failEl = document.createElement('div'); failEl.className = 'fail-message'; failEl.textContent = message; document.body.appendChild(failEl); setTimeout(() => { failEl.remove(); }, 2000); } _spawnMiniEnemy() { const miniEnemy = document.createElement('div'); miniEnemy.className = 'mini-enemy'; miniEnemy.textContent = '👚'; const mainEnemy = document.querySelector('.enemy-character'); const rect = mainEnemy.getBoundingClientRect(); miniEnemy.style.position = 'fixed'; miniEnemy.style.left = (rect.left + Math.random() * 200 - 100) + 'px'; miniEnemy.style.top = (rect.top + Math.random() * 200 - 100) + 'px'; document.body.appendChild(miniEnemy); setTimeout(() => { miniEnemy.remove(); }, 5000); this._enemyHP = Math.min(this._config.maxEnemyHP, this._enemyHP + 5); this._updateEnemyHealth(); } _wizardTakesDamage() { this._playerHP = Math.max(0, this._playerHP - 10); document.getElementById('player-health').style.width = this._playerHP + '%'; document.body.classList.add('screen-shake'); setTimeout(() => { document.body.classList.remove('screen-shake'); }, 500); const damageEl = document.createElement('div'); damageEl.className = 'damage-number'; damageEl.textContent = '-10'; damageEl.style.color = '#ff4757'; const wizardChar = document.querySelector('.wizard-character'); const rect = wizardChar.getBoundingClientRect(); damageEl.style.position = 'fixed'; damageEl.style.left = rect.left + rect.width/2 + 'px'; damageEl.style.top = rect.top + 'px'; document.body.appendChild(damageEl); setTimeout(() => { damageEl.remove(); }, 1500); if (this._playerHP <= 0) { setTimeout(() => { this._handleDefeat(); }, 1000); } } _triggerMagicQuirk() { const numQuirks = 2 + Math.floor(Math.random() * 2); for (let i = 0; i < numQuirks; i++) { setTimeout(() => { const quirk = document.createElement('div'); quirk.className = 'magic-quirk'; const x = 20 + Math.random() * 60; const y = 20 + Math.random() * 60; quirk.style.left = x + '%'; quirk.style.top = y + '%'; quirk.style.transform = 'translate(-50%, -50%)'; document.body.appendChild(quirk); setTimeout(() => { quirk.remove(); }, 2000); }, i * 300); } setTimeout(() => { this._updateWordBank(); }, 1000); } _summonFlyingBirds() { const birds = ['ðŸĶ', '🕊ïļ', 'ðŸĶ…', 'ðŸĶœ', '🐧', 'ðŸĶ†', 'ðŸĶĒ', '🐓', 'ðŸĶƒ', 'ðŸĶš']; const paths = ['bird-path-1', 'bird-path-2', 'bird-path-3']; const numBirds = 3 + Math.floor(Math.random() * 2); for (let i = 0; i < numBirds; i++) { setTimeout(() => { const bird = document.createElement('div'); const pathClass = paths[i % paths.length]; bird.className = `flying-bird ${pathClass}`; bird.textContent = birds[Math.floor(Math.random() * birds.length)]; document.body.appendChild(bird); setTimeout(() => { bird.remove(); }, 15000); }, i * 500); } } _updateEnemyHealth() { const healthBar = document.getElementById('enemy-health'); const percentage = (this._enemyHP / this._config.maxEnemyHP) * 100; healthBar.style.width = percentage + '%'; } _getRandomAttackTime() { return this._config.enemyAttackInterval.min + Math.random() * (this._config.enemyAttackInterval.max - this._config.enemyAttackInterval.min); } _startEnemyAttackSystem() { this._scheduleNextEnemyAttack(); } _scheduleNextEnemyAttack() { this._enemyAttackTimer = setTimeout(() => { this._executeEnemyAttack(); this._scheduleNextEnemyAttack(); }, this._getRandomAttackTime()); } _executeEnemyAttack() { const enemyChar = document.querySelector('.enemy-character'); this._showEnemyAttackWarning(); enemyChar.classList.add('enemy-charging'); setTimeout(() => { enemyChar.classList.remove('enemy-charging'); this._dealEnemyDamage(); this._showEnemyAttackEffect(); }, 2000); } _showEnemyAttackWarning() { const enemyChar = document.querySelector('.enemy-character'); const existingWarning = enemyChar.querySelector('.enemy-attack-warning'); if (existingWarning) { existingWarning.remove(); } const warning = document.createElement('div'); warning.className = 'enemy-attack-warning'; warning.textContent = '⚠ïļ INCOMING ATTACK!'; enemyChar.style.position = 'relative'; enemyChar.appendChild(warning); setTimeout(() => { warning.remove(); }, 2000); } _dealEnemyDamage() { const damage = this._config.enemyDamage.min + Math.floor(Math.random() * (this._config.enemyDamage.max - this._config.enemyDamage.min + 1)); this._playerHP = Math.max(0, this._playerHP - damage); document.getElementById('player-health').style.width = this._playerHP + '%'; document.body.classList.add('screen-shake'); setTimeout(() => { document.body.classList.remove('screen-shake'); }, 500); const damageEl = document.createElement('div'); damageEl.className = 'damage-number'; damageEl.textContent = `-${damage}`; damageEl.style.color = '#ff4757'; const wizardChar = document.querySelector('.wizard-character'); const rect = wizardChar.getBoundingClientRect(); damageEl.style.position = 'fixed'; damageEl.style.left = rect.left + rect.width/2 + 'px'; damageEl.style.top = rect.top + 'px'; document.body.appendChild(damageEl); setTimeout(() => { damageEl.remove(); }, 1500); // Emit enemy attack event this._eventBus.emit('wizard-spell-caster:enemy-attack', { gameId: 'wizard-spell-caster', instanceId: this.name, damage, playerHP: this._playerHP }, this.name); if (this._playerHP <= 0) { setTimeout(() => { this._handleDefeat(); }, 1000); } } _showEnemyAttackEffect() { const effect = document.createElement('div'); effect.className = 'enemy-attack-effect'; const wizardChar = document.querySelector('.wizard-character'); const rect = wizardChar.getBoundingClientRect(); effect.style.position = 'fixed'; effect.style.left = rect.left + rect.width/2 - 75 + 'px'; effect.style.top = rect.top + rect.height/2 - 75 + 'px'; document.body.appendChild(effect); setTimeout(() => { effect.remove(); }, 1000); } _handleVictory() { if (this._enemyAttackTimer) { clearTimeout(this._enemyAttackTimer); this._enemyAttackTimer = null; } const bonusScore = 1000; this._score += bonusScore; const victoryScreen = document.createElement('div'); victoryScreen.className = 'victory-screen'; victoryScreen.innerHTML = `
🎉 VICTORY! 🎉
You defeated the Grammar Demon!
Final Score: ${this._score}
Victory Bonus: +${bonusScore}
`; this._config.container.appendChild(victoryScreen); // Add event listeners victoryScreen.querySelector('#play-again-btn').addEventListener('click', () => { victoryScreen.remove(); this._restartGame(); }); victoryScreen.querySelector('#exit-victory-btn').addEventListener('click', () => { this._eventBus.emit('navigation:navigate', { path: '/games' }, 'Bootstrap'); }); // Emit victory event this._eventBus.emit('game:completed', { gameId: 'wizard-spell-caster', instanceId: this.name, score: this._score, spellsCast: this._spellCount, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }, this.name); } _handleDefeat() { if (this._enemyAttackTimer) { clearTimeout(this._enemyAttackTimer); this._enemyAttackTimer = null; } const defeatScreen = document.createElement('div'); defeatScreen.className = 'defeat-screen'; defeatScreen.innerHTML = `
💀 DEFEATED 💀
The Grammar Demon proved too strong!
Final Score: ${this._score}
`; this._config.container.appendChild(defeatScreen); // Add event listeners defeatScreen.querySelector('#try-again-btn').addEventListener('click', () => { defeatScreen.remove(); this._restartGame(); }); defeatScreen.querySelector('#exit-defeat-btn').addEventListener('click', () => { this._eventBus.emit('navigation:navigate', { path: '/games' }, 'Bootstrap'); }); // Emit defeat event this._eventBus.emit('game:completed', { gameId: 'wizard-spell-caster', instanceId: this.name, score: this._score, result: 'defeat', spellsCast: this._spellCount, duration: this._gameStartTime ? Date.now() - this._gameStartTime : 0 }, this.name); } _restartGame() { // Reset game state this._score = 0; this._enemyHP = this._config.maxEnemyHP; this._playerHP = this._config.maxPlayerHP; this._spellStartTime = Date.now(); this._averageSpellTime = 0; this._spellCount = 0; this._selectedSpell = null; this._selectedWords = []; // Update UI document.getElementById('current-score').textContent = this._score; document.getElementById('player-health').style.width = '100%'; document.getElementById('enemy-health').style.width = '100%'; // Restart enemy attacks this._startEnemyAttackSystem(); // Generate new spells this._generateNewSpells(); } _showError(message) { if (this._config.container) { this._config.container.innerHTML = `

❌ Wizard Spell Caster Error

${message}

This game requires sentences for spell construction.

`; } } _handlePause() { if (this._enemyAttackTimer) { clearTimeout(this._enemyAttackTimer); this._enemyAttackTimer = null; } this._eventBus.emit('game:paused', { instanceId: this.name }, this.name); } _handleResume() { this._startEnemyAttackSystem(); this._eventBus.emit('game:resumed', { instanceId: this.name }, this.name); } } export default WizardSpellCaster;