Class_generator/src/games/WizardSpellCaster.js
StillHammer 05142bdfbc Implement comprehensive AI text report/export system
- Add AIReportSystem.js for detailed AI response capture and report generation
- Add AIReportInterface.js UI component for report access and export
- Integrate AI reporting into LLMValidator and SmartPreviewOrchestrator
- Add missing modules to Application.js configuration (unifiedDRS, smartPreviewOrchestrator)
- Create missing content/chapters/sbs.json for book metadata
- Enhance Application.js with debug logging for module loading
- Add multi-format export capabilities (text, HTML, JSON)
- Implement automatic learning insights extraction from AI feedback
- Add session management and performance tracking for AI reports

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 21:24:13 +08:00

1838 lines
62 KiB
JavaScript

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 = `
<div class="wizard-game-wrapper">
<div class="wizard-hud">
<div class="wizard-stats">
<div>
<div>Wizard HP</div>
<div class="health-bar">
<div class="health-fill" id="player-health" style="width: 100%"></div>
</div>
</div>
<div>
<div>Score: <span id="current-score">0</span></div>
</div>
</div>
<div class="wizard-stats">
<div>
<div>Enemy HP</div>
<div class="health-bar">
<div class="health-fill" id="enemy-health" style="width: 100%"></div>
</div>
</div>
<button class="exit-wizard-btn" id="exit-wizard">
<span>←</span> Exit
</button>
</div>
</div>
<div class="battle-area">
<div class="wizard-side">
<div class="wizard-character">🧙‍♂️</div>
<div>Wizard Master</div>
</div>
<div class="enemy-side">
<div class="enemy-character">👹</div>
<div>Grammar Demon</div>
</div>
</div>
<div class="spell-casting-area">
<div class="spell-selection" id="spell-selection">
<!-- Spell cards will be populated here -->
</div>
<div class="sentence-builder" id="sentence-builder">
<div style="color: #ffd700; margin-bottom: 10px;">Form your spell incantation:</div>
<div id="current-sentence" style="font-size: 18px; min-height: 30px;"></div>
</div>
<div class="word-bank" id="word-bank">
<!-- Words will be populated here -->
</div>
<button class="cast-button" id="cast-button" disabled>🔥 CAST SPELL 🔥</button>
</div>
</div>
`;
}
_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) => `
<div class="spell-card" data-spell-index="${index}">
<div class="spell-type">${spell.icon} ${spell.name}</div>
<div style="margin: 10px 0; font-size: 14px;">${spell.translation}</div>
<div class="spell-damage">${spell.damage} damage</div>
</div>
`).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 = '<div style="color: #ffd700;">Select a spell first</div>';
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 `
<div class="word-tile" data-word-index="${index}" data-word="${wordData.word}" data-unique-id="${uniqueId}">
${wordData.word}
</div>
`;
}).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 = `
<div class="result-title">🎉 VICTORY! 🎉</div>
<div style="font-size: 24px; margin-bottom: 10px;">You defeated the Grammar Demon!</div>
<div style="font-size: 18px; margin-bottom: 10px;">Final Score: ${this._score}</div>
<div style="font-size: 16px; margin-bottom: 20px;">Victory Bonus: +${bonusScore}</div>
<div>
<button class="game-result-btn victory" id="play-again-btn">🔄 Play Again</button>
<button class="game-result-btn victory" id="exit-victory-btn">🏠 Back to Games</button>
</div>
`;
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 = `
<div class="result-title">💀 DEFEATED 💀</div>
<div style="font-size: 24px; margin-bottom: 10px;">The Grammar Demon proved too strong!</div>
<div style="font-size: 18px; margin-bottom: 20px;">Final Score: ${this._score}</div>
<div>
<button class="game-result-btn defeat" id="try-again-btn">🔄 Try Again</button>
<button class="game-result-btn defeat" id="exit-defeat-btn">🏠 Back to Games</button>
</div>
`;
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 = `
<div class="game-error">
<h3>❌ Wizard Spell Caster Error</h3>
<p>${message}</p>
<p>This game requires sentences for spell construction.</p>
<button class="back-btn" onclick="history.back()">← Go Back</button>
</div>
`;
}
}
_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;