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 = `
${message}
This game requires sentences for spell construction.