From e4d7e838d5dc3d95eec075d4ad96dd4f00ebcbca Mon Sep 17 00:00:00 2001 From: StillHammer Date: Sat, 18 Oct 2025 14:09:13 +0800 Subject: [PATCH] Enhance game modules with visual effects and improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add visual enhancements including fireball animations, improved rendering, and physics updates across multiple game modules (WizardSpellCaster, MarioEducational, WordDiscovery, WordStorm, RiverRun, GrammarDiscovery). ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/content/SBS-level-1.js | 2 +- .../MarioEducational/PhysicsEngine.js | 24 +- src/gameHelpers/MarioEducational/Renderer.js | 37 +- .../MarioEducational/enemies/PiranhaPlant.js | 25 +- src/games/GrammarDiscovery.js | 74 +- src/games/MarioEducational.js | 146 +++- src/games/RiverRun.js | 124 ++- src/games/WizardSpellCaster.js | 764 +++++++++++++++++- src/games/WordDiscovery.js | 407 ++++++++-- src/games/WordStorm.js | 100 ++- 10 files changed, 1538 insertions(+), 165 deletions(-) diff --git a/src/content/SBS-level-1.js b/src/content/SBS-level-1.js index 3793ab0..6c8cad8 100644 --- a/src/content/SBS-level-1.js +++ b/src/content/SBS-level-1.js @@ -240,7 +240,7 @@ window.ContentModules.SBSLevel1 = { story: { title: "To Be: Introduction - ๅŠจ่ฏBe็š„ไป‹็ป", - totalSentences: 50, + totalSentences: 13, chapters: [ { title: "Chapter 1: Vocabulary Preview - ็ฌฌไธ€็ซ ๏ผš่ฏๆฑ‡้ข„่งˆ", diff --git a/src/gameHelpers/MarioEducational/PhysicsEngine.js b/src/gameHelpers/MarioEducational/PhysicsEngine.js index deb819e..926e047 100644 --- a/src/gameHelpers/MarioEducational/PhysicsEngine.js +++ b/src/gameHelpers/MarioEducational/PhysicsEngine.js @@ -223,10 +223,19 @@ export class PhysicsEngine { if (this.isColliding(mario, enemy)) { // Check if Mario jumped on enemy if (mario.velocityY > 0 && mario.y < enemy.y + enemy.height / 2) { - // Enemy defeated + // Bounce on enemy mario.velocityY = -8; // Bounce - if (onEnemyDefeat) onEnemyDefeat(index); - if (onAddParticles) onAddParticles(enemy.x, enemy.y, '#FFD700'); + + // Only defeat enemy if it doesn't have a helmet + if (!enemy.hasHelmet) { + if (onEnemyDefeat) onEnemyDefeat(index); + if (onAddParticles) onAddParticles(enemy.x, enemy.y, '#FFD700'); + console.log(`๐Ÿ‘พ Mario defeated enemy`); + } else { + // Helmet enemy - just bounce, don't defeat + if (onAddParticles) onAddParticles(enemy.x, enemy.y, '#4169E1'); + console.log(`๐Ÿ›ก๏ธ Mario bounced on helmet enemy (not defeated)`); + } } else { // Mario hit by enemy console.log(`๐Ÿ‘พ Mario hit by enemy - restarting level`); @@ -253,9 +262,14 @@ export class PhysicsEngine { } } - // Check if stepping on flattened plant + // Check if stepping on flattened plant (acts as platform) if (plant.flattened && this.isColliding(mario, plant)) { - mario.onGround = true; + // Mario lands on top of flattened plant + if (mario.velocityY > 0 && mario.y < plant.y + plant.height / 2) { + mario.y = plant.y - mario.height; + mario.velocityY = 0; + mario.onGround = true; + } } }); diff --git a/src/gameHelpers/MarioEducational/Renderer.js b/src/gameHelpers/MarioEducational/Renderer.js index ff9200c..a680e6e 100644 --- a/src/gameHelpers/MarioEducational/Renderer.js +++ b/src/gameHelpers/MarioEducational/Renderer.js @@ -148,7 +148,7 @@ export class Renderer { renderEnemies(ctx, enemies) { enemies.forEach(enemy => { // Enemy body - ctx.fillStyle = '#FF6B6B'; + ctx.fillStyle = enemy.color || '#FF6B6B'; ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height); // Eyes @@ -161,6 +161,15 @@ export class Renderer { ctx.fillRect(enemy.x + 7, enemy.y + 7, 4, 4); ctx.fillRect(enemy.x + enemy.width - 11, enemy.y + 7, 4, 4); + // Helmet (for koopa type) + if (enemy.hasHelmet) { + ctx.fillStyle = '#FFD700'; // Gold helmet + ctx.fillRect(enemy.x + 2, enemy.y - 4, enemy.width - 4, 6); + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.strokeRect(enemy.x + 2, enemy.y - 4, enemy.width - 4, 6); + } + // Border ctx.strokeStyle = '#000'; ctx.lineWidth = 2; @@ -205,7 +214,17 @@ export class Renderer { plants.forEach(plant => { if (!plant.visible) return; - const pipeHeight = 60; + // If flattened, render as flat green patch (platform) + if (plant.flattened) { + ctx.fillStyle = '#228B22'; + ctx.fillRect(plant.x, plant.y + plant.height - 5, plant.width, 5); + ctx.strokeStyle = '#000'; + ctx.lineWidth = 1; + ctx.strokeRect(plant.x, plant.y + plant.height - 5, plant.width, 5); + return; + } + + const pipeHeight = 40; // Reduced from 60 const pipeY = plant.y + plant.height - pipeHeight; // Pipe @@ -214,18 +233,18 @@ export class Renderer { // Pipe rim ctx.fillStyle = '#3A9F3A'; - ctx.fillRect(plant.x - 5, pipeY, plant.width + 10, 10); + ctx.fillRect(plant.x - 3, pipeY, plant.width + 6, 8); // Reduced from -5, +10, 10 // Pipe border ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.strokeRect(plant.x, pipeY, plant.width, pipeHeight); - ctx.strokeRect(plant.x - 5, pipeY, plant.width + 10, 10); + ctx.strokeRect(plant.x - 3, pipeY, plant.width + 6, 8); // Plant head (if extended) if (plant.extended > 0) { const headY = pipeY - plant.extended; - const headSize = 30; + const headSize = 20; // Reduced from 30 // Head ctx.fillStyle = '#FF0000'; @@ -237,20 +256,20 @@ export class Renderer { // Spots ctx.fillStyle = '#FFF'; ctx.beginPath(); - ctx.arc(plant.x + plant.width / 2 - 10, headY - 5, 6, 0, Math.PI * 2); - ctx.arc(plant.x + plant.width / 2 + 10, headY - 5, 6, 0, Math.PI * 2); + ctx.arc(plant.x + plant.width / 2 - 7, headY - 3, 4, 0, Math.PI * 2); + ctx.arc(plant.x + plant.width / 2 + 7, headY - 3, 4, 0, Math.PI * 2); ctx.fill(); // Mouth (open/close animation) const mouthOpen = Math.sin(Date.now() / 200) > 0; if (mouthOpen) { ctx.fillStyle = '#000'; - ctx.fillRect(plant.x + plant.width / 2 - 12, headY + 8, 24, 8); + ctx.fillRect(plant.x + plant.width / 2 - 8, headY + 5, 16, 6); } // Stem ctx.fillStyle = '#2D882D'; - ctx.fillRect(plant.x + plant.width / 2 - 5, headY, 10, plant.extended); + ctx.fillRect(plant.x + plant.width / 2 - 3, headY, 6, plant.extended); } }); } diff --git a/src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js b/src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js index d3c8a77..79264b9 100644 --- a/src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js +++ b/src/gameHelpers/MarioEducational/enemies/PiranhaPlant.js @@ -27,16 +27,16 @@ export class PiranhaPlant { plants.push({ x: plantX, - y: platform.y - 40, // Plant height above platform - width: 30, - height: 40, + y: platform.y - 25, // Plant height above platform + width: 20, + height: 25, color: '#228B22', // Forest green lastShot: 0, shootCooldown: 2000 + Math.random() * 1000, // 2-3 second intervals type: 'piranha', visible: true, extended: 0, // For animation (how much plant extends from pipe) - maxExtension: 40, + maxExtension: 25, extending: true }); @@ -57,6 +57,19 @@ export class PiranhaPlant { const currentTime = Date.now(); plants.forEach(plant => { + // If plant is flattened, count down and make it a platform + if (plant.flattened) { + if (plant.flattenedTimer > 0) { + plant.flattenedTimer--; + } else { + // Plant recovers after timer expires + plant.flattened = false; + plant.extended = 0; + plant.extending = true; + } + return; // Don't animate or shoot while flattened + } + // Animate plant extension/retraction if (plant.extending) { plant.extended = Math.min(plant.extended + 1, plant.maxExtension); @@ -109,9 +122,9 @@ export class PiranhaPlant { if (!plant.visible) continue; // Only check collision when plant is extended - if (plant.extended > 20) { + if (plant.extended > 15) { const headY = plant.y - plant.extended; - const headRadius = 30; + const headRadius = 20; // Simple circle-rectangle collision const closestX = Math.max(mario.x, Math.min(plant.x + plant.width / 2, mario.x + mario.width)); diff --git a/src/games/GrammarDiscovery.js b/src/games/GrammarDiscovery.js index ff2d267..0bde951 100644 --- a/src/games/GrammarDiscovery.js +++ b/src/games/GrammarDiscovery.js @@ -788,7 +788,7 @@ class GrammarDiscovery extends Module {
${example.pronunciation || example.prononciation || ''}
${example.explanation || example.breakdown || ''}
-
@@ -877,7 +877,7 @@ class GrammarDiscovery extends Module {
${example.pronunciation || example.prononciation || ''}
${example.explanation || example.breakdown || ''}
-
@@ -1073,15 +1073,75 @@ class GrammarDiscovery extends Module { this._showConceptSelector(); } - _speakText(text, lang = 'zh-CN') { + async _speakText(text, lang = null) { if ('speechSynthesis' in window) { - const utterance = new SpeechSynthesisUtterance(text); - utterance.lang = lang; - utterance.rate = 0.8; - speechSynthesis.speak(utterance); + try { + // Cancel any ongoing speech + speechSynthesis.cancel(); + + const utterance = new SpeechSynthesisUtterance(text); + + // Get target language from content, fallback to parameter or default + const targetLanguage = this._content?.language || lang || 'zh-CN'; + utterance.lang = targetLanguage; + utterance.rate = 0.8; // Slightly slower for clarity + utterance.pitch = 1.0; + utterance.volume = 0.8; + + // Wait for voices to be loaded before selecting one + const voices = await this._getVoices(); + const langPrefix = targetLanguage.split('-')[0]; + const matchingVoice = voices.find(voice => + voice.lang.startsWith(langPrefix) && voice.default + ) || voices.find(voice => voice.lang.startsWith(langPrefix)); + + if (matchingVoice) { + utterance.voice = matchingVoice; + console.log(`๐Ÿ”Š Using voice: ${matchingVoice.name} (${matchingVoice.lang}) for language: ${targetLanguage}`); + } else { + console.warn(`๐Ÿ”Š No voice found for language: ${targetLanguage}, available:`, voices.map(v => v.lang)); + } + + speechSynthesis.speak(utterance); + } catch (error) { + console.warn('TTS error:', error); + } } } + /** + * Get available TTS voices, waiting for them to load if necessary + * @returns {Promise} Array of available voices + */ + _getVoices() { + return new Promise((resolve) => { + let voices = window.speechSynthesis.getVoices(); + + // If voices are already loaded, return them immediately + if (voices.length > 0) { + resolve(voices); + return; + } + + // Otherwise, wait for voiceschanged event + const voicesChangedHandler = () => { + voices = window.speechSynthesis.getVoices(); + if (voices.length > 0) { + window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler); + resolve(voices); + } + }; + + window.speechSynthesis.addEventListener('voiceschanged', voicesChangedHandler); + + // Fallback timeout in case voices never load + setTimeout(() => { + window.speechSynthesis.removeEventListener('voiceschanged', voicesChangedHandler); + resolve(window.speechSynthesis.getVoices()); + }, 1000); + }); + } + _shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { diff --git a/src/games/MarioEducational.js b/src/games/MarioEducational.js index ea5ba1b..727fa17 100644 --- a/src/games/MarioEducational.js +++ b/src/games/MarioEducational.js @@ -112,6 +112,14 @@ class MarioEducational extends Module { // Input handlers (need to be declared before seal) this._handleKeyDown = null; this._handleKeyUp = null; + this._handleMouseDown = null; + this._handleMouseUp = null; + this._handleMouseMove = null; + + // Mouse control state + this._mousePressed = false; + this._mouseTarget = { x: null, y: null }; + this._mouseWorldPos = { x: null, y: null }; // UI elements (need to be declared before seal) this._uiOverlay = null; @@ -229,7 +237,7 @@ class MarioEducational extends Module { this._generateAllLevels(); // Start first level - this._startLevel(5); // Start at level 6 (index 5) to continue boss work + this._startLevel(0); // Start at level 1 // Setup game UI this._setupGameUI(); @@ -260,6 +268,12 @@ class MarioEducational extends Module { // Remove event listeners document.removeEventListener('keydown', this._handleKeyDown); document.removeEventListener('keyup', this._handleKeyUp); + if (this._canvas) { + this._canvas.removeEventListener('mousedown', this._handleMouseDown); + this._canvas.removeEventListener('mouseup', this._handleMouseUp); + this._canvas.removeEventListener('mousemove', this._handleMouseMove); + this._canvas.removeEventListener('mouseleave', this._handleMouseUp); + } // Clear canvas if (this._canvas && this._canvas.parentNode) { @@ -419,8 +433,55 @@ class MarioEducational extends Module { this._keys[e.code] = false; }; + // Mouse handlers for click and drag controls + this._handleMouseDown = (e) => { + if (this._isGameOver || this._isPaused || this._isQuestionActive) return; + + const rect = this._canvas.getBoundingClientRect(); + const canvasX = e.clientX - rect.left; + const canvasY = e.clientY - rect.top; + + // Convert to world coordinates (account for camera) + this._mouseWorldPos.x = canvasX + this._camera.x; + this._mouseWorldPos.y = canvasY; + this._mousePressed = true; + + console.log(`๐Ÿ–ฑ๏ธ Mouse down at world: (${this._mouseWorldPos.x.toFixed(0)}, ${this._mouseWorldPos.y.toFixed(0)})`); + }; + + this._handleMouseUp = (e) => { + this._mousePressed = false; + this._mouseTarget.x = null; + this._mouseTarget.y = null; + console.log(`๐Ÿ–ฑ๏ธ Mouse released`); + }; + + this._handleMouseMove = (e) => { + if (!this._mousePressed) return; + if (this._isGameOver || this._isPaused || this._isQuestionActive) return; + + const rect = this._canvas.getBoundingClientRect(); + const canvasX = e.clientX - rect.left; + const canvasY = e.clientY - rect.top; + + // Convert to world coordinates + this._mouseWorldPos.x = canvasX + this._camera.x; + this._mouseWorldPos.y = canvasY; + }; + document.addEventListener('keydown', this._handleKeyDown); document.addEventListener('keyup', this._handleKeyUp); + + if (this._canvas) { + this._canvas.addEventListener('mousedown', this._handleMouseDown); + this._canvas.addEventListener('mouseup', this._handleMouseUp); + this._canvas.addEventListener('mousemove', this._handleMouseMove); + // Also handle mouse leaving canvas + this._canvas.addEventListener('mouseleave', this._handleMouseUp); + console.log('๐Ÿ–ฑ๏ธ Mouse event listeners attached to canvas'); + } else { + console.error('โŒ Canvas not found when setting up mouse handlers!'); + } } @@ -541,6 +602,7 @@ class MarioEducational extends Module { // Generate simple enemies (avoid walls) let helmetEnemyPlaced = false; + console.log(`๐ŸŽฎ Level ${index + 1}: Generating ${difficulty} enemies (helmetEnemy condition: index=${index} >= 1)`); for (let i = 0; i < difficulty; i++) { let enemyPlaced = false; @@ -570,7 +632,7 @@ class MarioEducational extends Module { if (!wouldOverlapWall && !wouldOverlapHole) { // Level 2+ gets exactly ONE helmet enemy per level - const isHelmetEnemy = index >= 1 && !helmetEnemyPlaced && i === 0; // Only first enemy can be helmet + const isHelmetEnemy = index >= 1 && !helmetEnemyPlaced; if (isHelmetEnemy) { helmetEnemyPlaced = true; @@ -587,7 +649,7 @@ class MarioEducational extends Module { hasHelmet: isHelmetEnemy }); enemyPlaced = true; - console.log(`โœ… Enemy ${i} placed at x=${enemyX.toFixed(0)}, y=${enemyY.toFixed(0)} on platform`); + console.log(`โœ… Enemy ${i} placed at x=${enemyX.toFixed(0)}, y=${enemyY.toFixed(0)} on platform - Type: ${isHelmetEnemy ? '๐Ÿ”ต KOOPA' : '๐Ÿ”ด Goomba'}`); } else { const reason = wouldOverlapWall ? 'wall' : 'hole'; console.log(`๐Ÿšซ Enemy ${i} attempt ${attempts} failed: ${reason}`); @@ -1666,7 +1728,51 @@ class MarioEducational extends Module { } _updateMarioMovement() { - PhysicsEngine.updateMarioMovement(this._mario, this._keys, this._config, this._isCelebrating, (sound) => soundSystem.play(sound)); + // Mouse controls take priority when mouse is pressed + if (this._mousePressed && this._mouseWorldPos.x !== null) { + this._handleMouseMovement(); + } else { + // Fallback to keyboard controls + PhysicsEngine.updateMarioMovement(this._mario, this._keys, this._config, this._isCelebrating, (sound) => soundSystem.play(sound)); + } + } + + _handleMouseMovement() { + if (this._isCelebrating) return; + + const targetX = this._mouseWorldPos.x; + const targetY = this._mouseWorldPos.y; + const marioX = this._mario.x + this._mario.width / 2; // Mario center + const marioY = this._mario.y + this._mario.height / 2; + + const distanceX = targetX - marioX; + const distanceY = targetY - marioY; + const totalDistance = Math.sqrt(distanceX * distanceX + distanceY * distanceY); + + // Dead zone - don't move if target is very close + if (totalDistance < 10) { + this._mario.velocityX *= 0.8; // Friction + return; + } + + // Horizontal movement + if (Math.abs(distanceX) > 5) { + if (distanceX > 0) { + this._mario.velocityX = this._config.moveSpeed; + this._mario.facing = 'right'; + } else { + this._mario.velocityX = -this._config.moveSpeed; + this._mario.facing = 'left'; + } + } + + // Jump if target is above Mario and Mario is on ground + if (distanceY < -20 && this._mario.onGround && Math.abs(distanceX) < 100) { + this._mario.velocityY = this._config.jumpForce; + this._mario.onGround = false; + soundSystem.play('jump'); + console.log(`๐Ÿ–ฑ๏ธ Auto-jump towards target`); + } } _updateMarioPhysics() { @@ -2363,6 +2469,38 @@ class MarioEducational extends Module { // Delegate rendering to helper renderer.render(this._ctx, gameState, this._config); + + // Draw mouse target indicator if mouse is pressed + if (this._mousePressed && this._mouseWorldPos.x !== null) { + this._drawMouseTarget(); + } + } + + _drawMouseTarget() { + const ctx = this._ctx; + const targetX = this._mouseWorldPos.x - this._camera.x; + const targetY = this._mouseWorldPos.y; + + // Draw a crosshair at the target position + ctx.save(); + ctx.strokeStyle = '#FFD700'; + ctx.lineWidth = 2; + ctx.globalAlpha = 0.8; + + // Draw circle + ctx.beginPath(); + ctx.arc(targetX, targetY, 15, 0, Math.PI * 2); + ctx.stroke(); + + // Draw crosshair lines + ctx.beginPath(); + ctx.moveTo(targetX - 20, targetY); + ctx.lineTo(targetX + 20, targetY); + ctx.moveTo(targetX, targetY - 20); + ctx.lineTo(targetX, targetY + 20); + ctx.stroke(); + + ctx.restore(); } /** diff --git a/src/games/RiverRun.js b/src/games/RiverRun.js index 15f3315..60e0457 100644 --- a/src/games/RiverRun.js +++ b/src/games/RiverRun.js @@ -62,6 +62,10 @@ class RiverRun extends Module { this._isMusicPlaying = false; this._musicLoopTimeout = null; + // Power-up state + this._slowTimeActive = false; + this._slowTimeTimeout = null; + Object.seal(this); } @@ -228,6 +232,13 @@ class RiverRun extends Module { this._animationFrame = null; } + // Clear slow time timeout if active + if (this._slowTimeTimeout) { + clearTimeout(this._slowTimeTimeout); + this._slowTimeTimeout = null; + } + this._slowTimeActive = false; + this._stopBackgroundMusic(); if (this._gameContainer) { @@ -729,6 +740,13 @@ class RiverRun extends Module { _missWord(wordElement) { soundSystem.play('enemy_defeat'); wordElement.classList.add('missed'); + + // Add shaking animation to the wrong word + wordElement.style.animation = 'wordShake 0.4s ease-in-out, wordMissed 0.6s ease-out forwards'; + + // Shake the entire screen + this._shakeScreen(); + this._loseLife(); setTimeout(() => { @@ -736,6 +754,17 @@ class RiverRun extends Module { }, 600); } + _shakeScreen() { + const riverGame = document.getElementById('river-game'); + if (!riverGame) return; + + riverGame.classList.add('screen-shake'); + + setTimeout(() => { + riverGame.classList.remove('screen-shake'); + }, 500); + } + _handlePowerUpCollision(powerUp, index) { this._activatePowerUp(powerUp.type); powerUp.element.remove(); @@ -745,9 +774,23 @@ class RiverRun extends Module { _activatePowerUp(type) { switch (type) { case 'slowTime': - this._speed *= 0.5; - setTimeout(() => { - this._speed *= 2; + // Clear any existing slowTime effect + if (this._slowTimeTimeout) { + clearTimeout(this._slowTimeTimeout); + } + + // Activate slow time effect + this._slowTimeActive = true; + soundSystem.play('powerup'); + + // Visual feedback - add a slow-time indicator to HUD + this._showSlowTimeIndicator(); + + // Reset after 3 seconds + this._slowTimeTimeout = setTimeout(() => { + this._slowTimeActive = false; + this._slowTimeTimeout = null; + this._hideSlowTimeIndicator(); }, 3000); break; } @@ -760,7 +803,10 @@ class RiverRun extends Module { // Progressive speed increase: starts at initialSpeed, increases gradually // Speed increases by 0.1 every 5 seconds (0.02 per second) const speedIncrease = secondsElapsed * 0.02; - this._speed = this._config.initialSpeed + speedIncrease; + const baseSpeed = this._config.initialSpeed + speedIncrease; + + // Apply slow-time multiplier if power-up is active + this._speed = this._slowTimeActive ? baseSpeed * 0.5 : baseSpeed; // Update level every 30 seconds const newLevel = Math.floor(timeElapsed / 30000) + 1; @@ -771,6 +817,37 @@ class RiverRun extends Module { } } + _showSlowTimeIndicator() { + const hud = document.querySelector('.river-run-hud .hud-right'); + if (!hud) return; + + // Remove any existing indicator + const existing = document.getElementById('slowtime-indicator'); + if (existing) existing.remove(); + + const indicator = document.createElement('div'); + indicator.id = 'slowtime-indicator'; + indicator.innerHTML = 'โฑ๏ธ SLOW TIME'; + indicator.style.cssText = ` + background: linear-gradient(45deg, #FF6B35, #F7931E); + color: white; + padding: 5px 15px; + border-radius: 15px; + font-size: 0.9em; + font-weight: bold; + animation: slowTimePulse 0.5s ease-in-out infinite alternate; + box-shadow: 0 0 15px rgba(255,107,53,0.8); + `; + hud.appendChild(indicator); + } + + _hideSlowTimeIndicator() { + const indicator = document.getElementById('slowtime-indicator'); + if (indicator) { + indicator.remove(); + } + } + _showPointsPopup(wordElement, points) { const rect = wordElement.getBoundingClientRect(); const riverCanvas = document.getElementById('river-canvas'); @@ -1109,6 +1186,22 @@ class RiverRun extends Module { cursor: crosshair; } + .river-run-wrapper.screen-shake { + animation: screenShake 0.5s ease-in-out; + } + + @keyframes screenShake { + 0%, 100% { + transform: translate(0, 0); + } + 10%, 30%, 50%, 70%, 90% { + transform: translate(-10px, 5px); + } + 20%, 40%, 60%, 80% { + transform: translate(10px, -5px); + } + } + .river-run-hud { position: absolute; top: 20px; @@ -1279,6 +1372,18 @@ class RiverRun extends Module { } } + @keyframes wordShake { + 0%, 100% { + transform: translateX(0) scale(1); + } + 10%, 30%, 50%, 70%, 90% { + transform: translateX(-10px) scale(1.05); + } + 20%, 40%, 60%, 80% { + transform: translateX(10px) scale(1.05); + } + } + .power-up { position: absolute; width: 35px; @@ -1300,6 +1405,17 @@ class RiverRun extends Module { to { transform: translateY(-8px) scale(1.05); } } + @keyframes slowTimePulse { + from { + transform: scale(1); + box-shadow: 0 0 15px rgba(255,107,53,0.8); + } + to { + transform: scale(1.05); + box-shadow: 0 0 25px rgba(255,107,53,1); + } + } + .game-over-modal { position: absolute; top: 50%; diff --git a/src/games/WizardSpellCaster.js b/src/games/WizardSpellCaster.js index da4c334..c136f3d 100644 --- a/src/games/WizardSpellCaster.js +++ b/src/games/WizardSpellCaster.js @@ -737,6 +737,91 @@ class WizardSpellCaster extends Module { animation: spellBlast 0.8s ease-out forwards, fireGlow 0.8s ease-out; } + .fireball-projectile { + position: fixed; + width: 60px; + height: 60px; + border-radius: 50%; + background: radial-gradient(circle at 30% 30%, #fff, #ffd700, #ff6b7a, #ff4757); + box-shadow: 0 0 40px #ff4757, 0 0 80px #ff6b7a, inset 0 0 20px #fff; + pointer-events: none; + z-index: 1000; + } + + .fireball-trail { + position: fixed; + width: 40px; + height: 40px; + border-radius: 50%; + background: radial-gradient(circle, #ff6b7a, transparent); + pointer-events: none; + z-index: 999; + animation: fadeTrail 0.5s ease-out forwards; + } + + @keyframes fadeTrail { + 0% { + opacity: 0.8; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(2); + } + } + + .fireball-spark { + position: fixed; + width: 8px; + height: 8px; + border-radius: 50%; + background: #ffd700; + box-shadow: 0 0 10px #ff4757; + pointer-events: none; + z-index: 998; + } + + .lightning-bolt { + position: fixed; + pointer-events: none; + z-index: 1000; + } + + .lightning-segment { + position: absolute; + background: linear-gradient(90deg, transparent, #fff, #ffd700, #fff, transparent); + box-shadow: 0 0 20px #ffd700, 0 0 40px #ffed4e; + transform-origin: left center; + } + + .lightning-spark { + position: fixed; + width: 10px; + height: 10px; + border-radius: 50%; + background: #fff; + box-shadow: 0 0 15px #ffd700, 0 0 30px #ffed4e; + pointer-events: none; + z-index: 999; + } + + .lightning-flash { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.3); + pointer-events: none; + z-index: 998; + animation: lightningFlash 0.3s ease-out; + } + + @keyframes lightningFlash { + 0%, 100% { opacity: 0; } + 50% { opacity: 1; } + } + .lightning-effect { background: radial-gradient(circle, #ffd700, #ffed4e, #fff200, transparent); filter: drop-shadow(0 0 25px #ffd700); @@ -791,6 +876,81 @@ class WizardSpellCaster extends Module { } } + .meteor-projectile { + position: fixed; + width: 80px; + height: 80px; + border-radius: 50%; + background: radial-gradient(circle at 25% 25%, #fff, #a29bfe, #6c5ce7, #5f3dc4); + box-shadow: 0 0 50px #6c5ce7, 0 0 100px #a29bfe, inset 0 0 30px rgba(255, 255, 255, 0.5); + pointer-events: none; + z-index: 1000; + } + + .meteor-trail { + position: fixed; + width: 60px; + height: 60px; + border-radius: 50%; + background: radial-gradient(circle, #a29bfe, #6c5ce7, transparent); + pointer-events: none; + z-index: 999; + animation: fadeTrail 0.6s ease-out forwards; + } + + .meteor-debris { + position: fixed; + width: 10px; + height: 10px; + background: linear-gradient(135deg, #a29bfe, #6c5ce7); + box-shadow: 0 0 15px #6c5ce7; + pointer-events: none; + z-index: 998; + } + + .meteor-spark { + position: fixed; + width: 12px; + height: 12px; + border-radius: 50%; + background: #a29bfe; + box-shadow: 0 0 20px #6c5ce7, 0 0 40px #a29bfe; + pointer-events: none; + z-index: 997; + } + + .meteor-shockwave { + position: fixed; + width: 20px; + height: 20px; + border: 3px solid #a29bfe; + border-radius: 50%; + box-shadow: 0 0 20px #6c5ce7, inset 0 0 20px #6c5ce7; + pointer-events: none; + z-index: 996; + transform: translate(-50%, -50%); + animation: meteorShockwave 0.8s ease-out forwards; + } + + @keyframes meteorShockwave { + 0% { + width: 20px; + height: 20px; + opacity: 1; + border-width: 5px; + } + 50% { + opacity: 0.6; + border-width: 3px; + } + 100% { + width: 200px; + height: 200px; + opacity: 0; + border-width: 1px; + } + } + .mini-enemy { position: absolute; width: 60px; @@ -1221,6 +1381,10 @@ class WizardSpellCaster extends Module { _renderSpellCards() { const container = document.getElementById('spell-selection'); + if (!container) { + console.warn('Spell selection container not found, skipping render'); + return; + } container.innerHTML = this._currentSpells.map((spell, index) => `
${spell.icon} ${spell.name}
@@ -1418,23 +1582,572 @@ class WizardSpellCaster extends Module { _showSpellEffect(type) { const enemyChar = document.querySelector('.enemy-character'); + if (!enemyChar) { + console.warn('Enemy character not found, skipping spell effect'); + return; + } 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); + // Special animations for different spell types + if (type === 'short' || type === 'fire') { + this._launchFireball(rect); + } else if (type === 'medium' || type === 'lightning') { + this._launchLightning(rect); + } else if (type === 'long' || type === 'meteor') { + this._launchMeteor(rect); + } else { + // Main spell effect for other types + 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); + + setTimeout(() => { + effect.remove(); + }, 800); + } // Enhanced effects based on spell type this._createSpellParticles(type, rect); this._triggerSpellAnimation(type, enemyChar); + } + _launchFireball(targetRect) { + const wizardChar = document.querySelector('.wizard-character'); + const wizardRect = wizardChar.getBoundingClientRect(); + + // Create fireball + const fireball = document.createElement('div'); + fireball.className = 'fireball-projectile'; + + const startX = wizardRect.left + wizardRect.width / 2 - 30; + const startY = wizardRect.top + wizardRect.height / 2 - 30; + const endX = targetRect.left + targetRect.width / 2 - 30; + const endY = targetRect.top + targetRect.height / 2 - 30; + + fireball.style.left = startX + 'px'; + fireball.style.top = startY + 'px'; + + document.body.appendChild(fireball); + + // Animation parameters + const duration = 500; // milliseconds + const startTime = Date.now(); + const trailInterval = 30; // Create trail every 30ms + let lastTrailTime = 0; + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + + // Easing function (ease-in-out) + const eased = progress < 0.5 + ? 2 * progress * progress + : -1 + (4 - 2 * progress) * progress; + + // Calculate current position + const currentX = startX + (endX - startX) * eased; + const currentY = startY + (endY - startY) * eased; + + fireball.style.left = currentX + 'px'; + fireball.style.top = currentY + 'px'; + + // Create trail effect + if (elapsed - lastTrailTime > trailInterval) { + this._createFireballTrail(currentX + 30, currentY + 30); + this._createFireballSparks(currentX + 30, currentY + 30); + lastTrailTime = elapsed; + } + + // Continue animation or finish + if (progress < 1) { + requestAnimationFrame(animate); + } else { + // Impact effect + fireball.remove(); + this._createFireballImpact(endX + 30, endY + 30); + } + }; + + animate(); + } + + _createFireballTrail(x, y) { + const trail = document.createElement('div'); + trail.className = 'fireball-trail'; + trail.style.left = (x - 20) + 'px'; + trail.style.top = (y - 20) + 'px'; + document.body.appendChild(trail); + + setTimeout(() => trail.remove(), 500); + } + + _createFireballSparks(x, y) { + const sparkCount = 3; + for (let i = 0; i < sparkCount; i++) { + const spark = document.createElement('div'); + spark.className = 'fireball-spark'; + + const angle = Math.random() * Math.PI * 2; + const distance = 20 + Math.random() * 30; + const sparkX = x + Math.cos(angle) * distance; + const sparkY = y + Math.sin(angle) * distance; + + spark.style.left = sparkX + 'px'; + spark.style.top = sparkY + 'px'; + + document.body.appendChild(spark); + + // Animate spark + const duration = 300 + Math.random() * 200; + const startTime = Date.now(); + const startSparkX = sparkX; + const startSparkY = sparkY; + const velocityX = (Math.random() - 0.5) * 100; + const velocityY = Math.random() * 50 - 100; + + const animateSpark = () => { + const elapsed = Date.now() - startTime; + const progress = elapsed / duration; + + if (progress < 1) { + const newX = startSparkX + velocityX * progress; + const newY = startSparkY + velocityY * progress + (progress * progress * 100); + spark.style.left = newX + 'px'; + spark.style.top = newY + 'px'; + spark.style.opacity = 1 - progress; + requestAnimationFrame(animateSpark); + } else { + spark.remove(); + } + }; + + animateSpark(); + } + } + + _createFireballImpact(x, y) { + // Main explosion + const explosion = document.createElement('div'); + explosion.className = 'spell-effect fire-effect'; + explosion.style.position = 'fixed'; + explosion.style.left = (x - 50) + 'px'; + explosion.style.top = (y - 50) + 'px'; + document.body.appendChild(explosion); + + setTimeout(() => explosion.remove(), 800); + + // Impact sparks + const impactSparkCount = 12; + for (let i = 0; i < impactSparkCount; i++) { + const angle = (i / impactSparkCount) * Math.PI * 2; + const spark = document.createElement('div'); + spark.className = 'fireball-spark'; + spark.style.left = x + 'px'; + spark.style.top = y + 'px'; + spark.style.width = '12px'; + spark.style.height = '12px'; + + document.body.appendChild(spark); + + const distance = 60 + Math.random() * 40; + const duration = 400; + const startTime = Date.now(); + + const animateImpactSpark = () => { + const elapsed = Date.now() - startTime; + const progress = elapsed / duration; + + if (progress < 1) { + const currentDistance = distance * progress; + const newX = x + Math.cos(angle) * currentDistance; + const newY = y + Math.sin(angle) * currentDistance; + spark.style.left = newX + 'px'; + spark.style.top = newY + 'px'; + spark.style.opacity = 1 - progress; + spark.style.transform = `scale(${1 - progress * 0.5})`; + requestAnimationFrame(animateImpactSpark); + } else { + spark.remove(); + } + }; + + animateImpactSpark(); + } + } + + _launchLightning(targetRect) { + const wizardChar = document.querySelector('.wizard-character'); + const wizardRect = wizardChar.getBoundingClientRect(); + + const startX = wizardRect.left + wizardRect.width / 2; + const startY = wizardRect.top + wizardRect.height / 2; + const endX = targetRect.left + targetRect.width / 2; + const endY = targetRect.top + targetRect.height / 2; + + // Screen flash + const flash = document.createElement('div'); + flash.className = 'lightning-flash'; + document.body.appendChild(flash); + setTimeout(() => flash.remove(), 300); + + // Create lightning bolt with jagged segments + const boltContainer = document.createElement('div'); + boltContainer.className = 'lightning-bolt'; + document.body.appendChild(boltContainer); + + const segments = 8; + const points = [{ x: startX, y: startY }]; + + // Generate jagged lightning path + for (let i = 1; i < segments; i++) { + const progress = i / segments; + const baseX = startX + (endX - startX) * progress; + const baseY = startY + (endY - startY) * progress; + + // Add random offset for jagged effect + const offsetX = (Math.random() - 0.5) * 60; + const offsetY = (Math.random() - 0.5) * 60; + + points.push({ + x: baseX + offsetX, + y: baseY + offsetY + }); + } + + points.push({ x: endX, y: endY }); + + // Draw lightning segments + for (let i = 0; i < points.length - 1; i++) { + const segment = document.createElement('div'); + segment.className = 'lightning-segment'; + + const dx = points[i + 1].x - points[i].x; + const dy = points[i + 1].y - points[i].y; + const length = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx) * (180 / Math.PI); + + segment.style.left = points[i].x + 'px'; + segment.style.top = points[i].y + 'px'; + segment.style.width = length + 'px'; + segment.style.height = (3 + Math.random() * 3) + 'px'; + segment.style.transform = `rotate(${angle}deg)`; + + boltContainer.appendChild(segment); + } + + // Create electric sparks along the path + for (let i = 0; i < 15; i++) { + setTimeout(() => { + const pointIndex = Math.floor(Math.random() * points.length); + const point = points[pointIndex]; + this._createLightningSpark(point.x, point.y); + }, i * 30); + } + + // Remove lightning bolt after animation setTimeout(() => { - effect.remove(); - }, 800); + boltContainer.remove(); + }, 400); + + // Impact effect + setTimeout(() => { + this._createLightningImpact(endX, endY); + }, 200); + } + + _createLightningSpark(x, y) { + const spark = document.createElement('div'); + spark.className = 'lightning-spark'; + spark.style.left = x + 'px'; + spark.style.top = y + 'px'; + document.body.appendChild(spark); + + const angle = Math.random() * Math.PI * 2; + const distance = 30 + Math.random() * 40; + const duration = 300; + const startTime = Date.now(); + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = elapsed / duration; + + if (progress < 1) { + const currentDist = distance * progress; + const newX = x + Math.cos(angle) * currentDist; + const newY = y + Math.sin(angle) * currentDist; + spark.style.left = newX + 'px'; + spark.style.top = newY + 'px'; + spark.style.opacity = 1 - progress; + requestAnimationFrame(animate); + } else { + spark.remove(); + } + }; + + animate(); + } + + _createLightningImpact(x, y) { + // Main impact explosion + const explosion = document.createElement('div'); + explosion.className = 'spell-effect lightning-effect'; + explosion.style.position = 'fixed'; + explosion.style.left = (x - 50) + 'px'; + explosion.style.top = (y - 50) + 'px'; + document.body.appendChild(explosion); + + setTimeout(() => explosion.remove(), 800); + + // Electric burst + for (let i = 0; i < 20; i++) { + const spark = document.createElement('div'); + spark.className = 'lightning-spark'; + const angle = (i / 20) * Math.PI * 2; + const distance = 40 + Math.random() * 60; + + spark.style.left = x + 'px'; + spark.style.top = y + 'px'; + document.body.appendChild(spark); + + const duration = 400 + Math.random() * 200; + const startTime = Date.now(); + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = elapsed / duration; + + if (progress < 1) { + const currentDist = distance * progress; + const newX = x + Math.cos(angle) * currentDist; + const newY = y + Math.sin(angle) * currentDist; + spark.style.left = newX + 'px'; + spark.style.top = newY + 'px'; + spark.style.opacity = 1 - progress; + spark.style.transform = `scale(${1 - progress * 0.7})`; + requestAnimationFrame(animate); + } else { + spark.remove(); + } + }; + + animate(); + } + } + + _launchMeteor(targetRect) { + // Create meteor starting from top of screen + const meteor = document.createElement('div'); + meteor.className = 'meteor-projectile'; + + // Random horizontal start position (from above the screen) + const startX = targetRect.left + targetRect.width / 2 + (Math.random() - 0.5) * 300; + const startY = -100; // Start above viewport + const endX = targetRect.left + targetRect.width / 2 - 40; + const endY = targetRect.top + targetRect.height / 2 - 40; + + meteor.style.left = startX + 'px'; + meteor.style.top = startY + 'px'; + + document.body.appendChild(meteor); + + // Animation parameters + const duration = 800; // milliseconds + const startTime = Date.now(); + const trailInterval = 25; + let lastTrailTime = 0; + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = Math.min(elapsed / duration, 1); + + // Accelerating fall (quadratic easing) + const eased = progress * progress; + + // Calculate current position + const currentX = startX + (endX - startX) * progress; + const currentY = startY + (endY - startY) * eased; + + meteor.style.left = currentX + 'px'; + meteor.style.top = currentY + 'px'; + + // Rotate meteor as it falls + meteor.style.transform = `rotate(${progress * 360}deg) scale(${1 + progress * 0.5})`; + + // Create trail effect + if (elapsed - lastTrailTime > trailInterval) { + this._createMeteorTrail(currentX + 40, currentY + 40); + this._createMeteorDebris(currentX + 40, currentY + 40); + lastTrailTime = elapsed; + } + + // Continue animation or finish + if (progress < 1) { + requestAnimationFrame(animate); + } else { + // Impact effect + meteor.remove(); + this._createMeteorImpact(endX + 40, endY + 40); + } + }; + + animate(); + } + + _createMeteorTrail(x, y) { + const trail = document.createElement('div'); + trail.className = 'meteor-trail'; + trail.style.left = (x - 30) + 'px'; + trail.style.top = (y - 30) + 'px'; + document.body.appendChild(trail); + + setTimeout(() => trail.remove(), 600); + } + + _createMeteorDebris(x, y) { + const debrisCount = 2; + for (let i = 0; i < debrisCount; i++) { + const debris = document.createElement('div'); + debris.className = 'meteor-debris'; + + // Debris flies off to the sides + const angle = Math.PI / 2 + (Math.random() - 0.5) * Math.PI; + const distance = 30 + Math.random() * 40; + + debris.style.left = x + 'px'; + debris.style.top = y + 'px'; + + document.body.appendChild(debris); + + const duration = 400 + Math.random() * 200; + const startTime = Date.now(); + + const animateDebris = () => { + const elapsed = Date.now() - startTime; + const progress = elapsed / duration; + + if (progress < 1) { + const currentDist = distance * progress; + const newX = x + Math.cos(angle) * currentDist; + const newY = y + Math.sin(angle) * currentDist + (progress * progress * 80); + debris.style.left = newX + 'px'; + debris.style.top = newY + 'px'; + debris.style.opacity = 1 - progress; + debris.style.transform = `rotate(${progress * 720}deg) scale(${1 - progress})`; + requestAnimationFrame(animateDebris); + } else { + debris.remove(); + } + }; + + animateDebris(); + } + } + + _createMeteorImpact(x, y) { + // Screen shake on impact + document.body.classList.add('screen-shake'); + setTimeout(() => document.body.classList.remove('screen-shake'), 500); + + // Main explosion crater effect + const explosion = document.createElement('div'); + explosion.className = 'spell-effect meteor-effect'; + explosion.style.position = 'fixed'; + explosion.style.left = (x - 50) + 'px'; + explosion.style.top = (y - 50) + 'px'; + document.body.appendChild(explosion); + + setTimeout(() => explosion.remove(), 800); + + // Shockwave rings + for (let i = 0; i < 3; i++) { + setTimeout(() => { + const shockwave = document.createElement('div'); + shockwave.className = 'meteor-shockwave'; + shockwave.style.left = x + 'px'; + shockwave.style.top = y + 'px'; + document.body.appendChild(shockwave); + + setTimeout(() => shockwave.remove(), 800); + }, i * 100); + } + + // Impact debris explosion + const debrisCount = 20; + for (let i = 0; i < debrisCount; i++) { + const debris = document.createElement('div'); + debris.className = 'meteor-debris'; + const angle = (i / debrisCount) * Math.PI * 2; + const distance = 50 + Math.random() * 80; + + debris.style.left = x + 'px'; + debris.style.top = y + 'px'; + debris.style.width = (6 + Math.random() * 8) + 'px'; + debris.style.height = (6 + Math.random() * 8) + 'px'; + + document.body.appendChild(debris); + + const duration = 600 + Math.random() * 400; + const startTime = Date.now(); + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = elapsed / duration; + + if (progress < 1) { + const currentDist = distance * progress; + const newX = x + Math.cos(angle) * currentDist; + const newY = y + Math.sin(angle) * currentDist - (50 * progress) + (progress * progress * 100); + debris.style.left = newX + 'px'; + debris.style.top = newY + 'px'; + debris.style.opacity = 1 - progress; + debris.style.transform = `rotate(${progress * 1080}deg) scale(${1 - progress * 0.5})`; + requestAnimationFrame(animate); + } else { + debris.remove(); + } + }; + + animate(); + } + + // Purple impact sparks + for (let i = 0; i < 12; i++) { + const spark = document.createElement('div'); + spark.className = 'meteor-spark'; + const angle = (i / 12) * Math.PI * 2; + const distance = 60 + Math.random() * 50; + + spark.style.left = x + 'px'; + spark.style.top = y + 'px'; + + document.body.appendChild(spark); + + const duration = 500; + const startTime = Date.now(); + + const animate = () => { + const elapsed = Date.now() - startTime; + const progress = elapsed / duration; + + if (progress < 1) { + const currentDist = distance * (1 - Math.pow(1 - progress, 3)); + const newX = x + Math.cos(angle) * currentDist; + const newY = y + Math.sin(angle) * currentDist; + spark.style.left = newX + 'px'; + spark.style.top = newY + 'px'; + spark.style.opacity = 1 - progress; + spark.style.transform = `scale(${1 - progress * 0.8})`; + requestAnimationFrame(animate); + } else { + spark.remove(); + } + }; + + animate(); + } } _createSpellParticles(type, enemyRect) { @@ -1599,7 +2312,10 @@ class WizardSpellCaster extends Module { _wizardTakesDamage() { this._playerHP = Math.max(0, this._playerHP - 10); - document.getElementById('player-health').style.width = this._playerHP + '%'; + const playerHealthEl = document.getElementById('player-health'); + if (playerHealthEl) { + playerHealthEl.style.width = this._playerHP + '%'; + } document.body.classList.add('screen-shake'); setTimeout(() => { @@ -1612,6 +2328,10 @@ class WizardSpellCaster extends Module { damageEl.style.color = '#ff4757'; const wizardChar = document.querySelector('.wizard-character'); + if (!wizardChar) { + console.warn('Wizard character not found, skipping damage display'); + return; + } const rect = wizardChar.getBoundingClientRect(); damageEl.style.position = 'fixed'; @@ -1741,7 +2461,10 @@ class WizardSpellCaster extends Module { 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 + '%'; + const playerHealthEl = document.getElementById('player-health'); + if (playerHealthEl) { + playerHealthEl.style.width = this._playerHP + '%'; + } document.body.classList.add('screen-shake'); setTimeout(() => { @@ -1754,6 +2477,10 @@ class WizardSpellCaster extends Module { damageEl.style.color = '#ff4757'; const wizardChar = document.querySelector('.wizard-character'); + if (!wizardChar) { + console.warn('Wizard character not found, skipping damage display'); + return; + } const rect = wizardChar.getBoundingClientRect(); damageEl.style.position = 'fixed'; @@ -1896,9 +2623,18 @@ class WizardSpellCaster extends Module { 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%'; + const currentScoreEl = document.getElementById('current-score'); + if (currentScoreEl) { + currentScoreEl.textContent = this._score; + } + const playerHealthEl = document.getElementById('player-health'); + if (playerHealthEl) { + playerHealthEl.style.width = '100%'; + } + const enemyHealthEl = document.getElementById('enemy-health'); + if (enemyHealthEl) { + enemyHealthEl.style.width = '100%'; + } // Restart enemy attacks this._startEnemyAttackSystem(); diff --git a/src/games/WordDiscovery.js b/src/games/WordDiscovery.js index f15a6e4..6745bb0 100644 --- a/src/games/WordDiscovery.js +++ b/src/games/WordDiscovery.js @@ -34,6 +34,15 @@ class WordDiscovery extends Module { this._correctAnswer = null; this._currentQuestion = null; + // Session progress tracking (5 discovery / 3 practice cycle) + this._sessionDiscoveryCount = 0; // Words discovered in current cycle + this._sessionPracticeCount = 0; // Practice questions answered in current cycle + this._totalDiscoveryCount = 0; // Total discoveries in session + this._totalPracticeCount = 0; // Total practice questions in session + this._cycleDiscoveryMax = 5; // Discover 5 words + this._cyclePracticeMax = 3; // Then 3 practice questions + this._totalWordsAvailable = 0; // Total vocab words (e.g., 76) + Object.seal(this); } @@ -134,12 +143,13 @@ class WordDiscovery extends Module { example: data.example })); - this._discoveredWords = []; - this._currentWordIndex = 0; - this._currentPhase = 'discovery'; + this._totalWordsAvailable = this._practiceWords.length; + + // Restore session or start fresh + this._restoreSession(); await this._preloadAssets(); - this._renderDiscoveryPhase(); + this._renderCurrentPhase(); // Emit game ready event this._eventBus.emit('game:ready', { @@ -240,6 +250,129 @@ class WordDiscovery extends Module { } } + /** + * Save current session state to sessionStorage + * @private + */ + _saveSession() { + const sessionData = { + currentPhase: this._currentPhase, + sessionDiscoveryCount: this._sessionDiscoveryCount, + sessionPracticeCount: this._sessionPracticeCount, + totalDiscoveryCount: this._totalDiscoveryCount, + totalPracticeCount: this._totalPracticeCount, + currentWordIndex: this._currentWordIndex, + discoveredWords: this._discoveredWords.map(w => w.word), + practiceCorrect: this._practiceCorrect, + practiceTotal: this._practiceTotal + }; + + sessionStorage.setItem('wordDiscovery_session', JSON.stringify(sessionData)); + } + + /** + * Restore session state from sessionStorage or start fresh + * @private + */ + _restoreSession() { + const savedData = sessionStorage.getItem('wordDiscovery_session'); + + if (savedData) { + try { + const session = JSON.parse(savedData); + this._currentPhase = session.currentPhase || 'discovery'; + this._sessionDiscoveryCount = session.sessionDiscoveryCount || 0; + this._sessionPracticeCount = session.sessionPracticeCount || 0; + this._totalDiscoveryCount = session.totalDiscoveryCount || 0; + this._totalPracticeCount = session.totalPracticeCount || 0; + this._currentWordIndex = session.currentWordIndex || 0; + this._practiceCorrect = session.practiceCorrect || 0; + this._practiceTotal = session.practiceTotal || 0; + + // Restore discovered words + if (session.discoveredWords && Array.isArray(session.discoveredWords)) { + this._discoveredWords = this._practiceWords.filter(w => + session.discoveredWords.includes(w.word) + ); + } + + console.log('๐Ÿ“– Session restored:', session); + } catch (error) { + console.warn('Failed to restore session:', error); + this._startFreshSession(); + } + } else { + this._startFreshSession(); + } + } + + /** + * Start a fresh session (reset all counters) + * @private + */ + _startFreshSession() { + this._discoveredWords = []; + this._currentWordIndex = 0; + this._currentPhase = 'discovery'; + this._sessionDiscoveryCount = 0; + this._sessionPracticeCount = 0; + this._totalDiscoveryCount = 0; + this._totalPracticeCount = 0; + this._practiceCorrect = 0; + this._practiceTotal = 0; + } + + /** + * Calculate total progress including both discovery and practice + * Total = totalWordsAvailable + (totalWordsAvailable * 3/5) + * @private + */ + _calculateTotalProgress() { + const maxDiscovery = this._totalWordsAvailable; + const maxPractice = Math.floor(this._totalWordsAvailable * 3 / 5); + const totalMax = maxDiscovery + maxPractice; + const currentProgress = this._totalDiscoveryCount + this._totalPracticeCount; + + return { + current: currentProgress, + max: totalMax, + percentage: totalMax > 0 ? (currentProgress / totalMax) * 100 : 0 + }; + } + + /** + * Determine if we should switch phases based on cycle counts + * @private + */ + _shouldSwitchPhase() { + if (this._currentPhase === 'discovery') { + return this._sessionDiscoveryCount >= this._cycleDiscoveryMax; + } else { + return this._sessionPracticeCount >= this._cyclePracticeMax; + } + } + + /** + * Render the appropriate phase based on current state + * @private + */ + _renderCurrentPhase() { + // Check if we've completed everything + const maxDiscovery = this._totalWordsAvailable; + const maxPractice = Math.floor(this._totalWordsAvailable * 3 / 5); + + if (this._totalDiscoveryCount >= maxDiscovery && this._totalPracticeCount >= maxPractice) { + this._showCompletionScreen(); + return; + } + + if (this._currentPhase === 'discovery') { + this._renderDiscoveryPhase(); + } else { + this._renderPracticePhase(); + } + } + async _preloadAssets() { for (const word of this._practiceWords) { if (word.audio) { @@ -271,20 +404,41 @@ class WordDiscovery extends Module { } _renderDiscoveryPhase() { - const word = this._practiceWords[this._currentWordIndex]; - if (!word) { - this._startPracticePhase(); + // Check if we've discovered all words + if (this._totalDiscoveryCount >= this._totalWordsAvailable) { + this._currentPhase = 'practice'; + this._renderPracticePhase(); return; } + // Check if we need to switch to practice (5 discoveries done in this cycle) + if (this._shouldSwitchPhase()) { + this._currentPhase = 'practice'; + this._sessionPracticeCount = 0; // Reset practice counter for new cycle + this._saveSession(); + this._renderPracticePhase(); + return; + } + + const word = this._practiceWords[this._currentWordIndex]; + if (!word) { + // No more words, finish discovery + this._currentPhase = 'practice'; + this._renderPracticePhase(); + return; + } + + const progress = this._calculateTotalProgress(); + this._gameContainer.innerHTML = `
-

Word Discovery

+

Word Discovery (${this._sessionDiscoveryCount}/${this._cycleDiscoveryMax} in cycle)

-
+
-

Progress: ${this._currentWordIndex + 1} / ${this._practiceWords.length}

+

Total Progress: ${progress.current} / ${progress.max}

+

After ${this._cycleDiscoveryMax} discoveries, ${this._cyclePracticeMax} practice questions

@@ -311,12 +465,6 @@ class WordDiscovery extends Module {
- -
- -
`; @@ -332,10 +480,13 @@ class WordDiscovery extends Module { const currentWord = this._practiceWords[this._currentWordIndex]; if (currentWord && !this._discoveredWords.find(w => w.word === currentWord.word)) { this._discoveredWords.push(currentWord); + this._sessionDiscoveryCount++; + this._totalDiscoveryCount++; } this._currentWordIndex++; - this._renderDiscoveryPhase(); + this._saveSession(); + this._renderCurrentPhase(); } _playAudio(word) { @@ -433,35 +584,49 @@ class WordDiscovery extends Module { }); } - _startPracticePhase() { - if (this._discoveredWords.length === 0) { - this._discoveredWords = [...this._practiceWords]; + _renderPracticePhase() { + // Check if we've completed all practice (max = totalWords * 3/5) + const maxPractice = Math.floor(this._totalWordsAvailable * 3 / 5); + if (this._totalPracticeCount >= maxPractice) { + // All practice done, switch back to discovery or finish + if (this._totalDiscoveryCount < this._totalWordsAvailable) { + this._currentPhase = 'discovery'; + this._sessionDiscoveryCount = 0; + this._saveSession(); + this._renderDiscoveryPhase(); + } else { + this._showCompletionScreen(); + } + return; } - this._currentPhase = 'practice'; - this._currentPracticeLevel = 0; - this._practiceCorrect = 0; - this._practiceTotal = 0; + // Check if we need to switch back to discovery (3 practice done in this cycle) + if (this._shouldSwitchPhase()) { + this._currentPhase = 'discovery'; + this._sessionDiscoveryCount = 0; // Reset discovery counter for new cycle + this._saveSession(); + this._renderDiscoveryPhase(); + return; + } - this._renderPracticeLevel(); - } + if (this._discoveredWords.length === 0) { + // No words to practice yet + this._currentPhase = 'discovery'; + this._renderDiscoveryPhase(); + return; + } - _renderPracticeLevel() { - const levels = ['Easy', 'Medium', 'Hard', 'Expert']; - const levelConfig = { - 0: { options: 4, type: 'translation' }, - 1: { options: 3, type: 'mixed' }, - 2: { options: 4, type: 'definition' }, - 3: { options: 4, type: 'context' } - }; - - const config = levelConfig[this._currentPracticeLevel]; - const levelName = levels[this._currentPracticeLevel]; + const progress = this._calculateTotalProgress(); + const config = { options: 4, type: 'translation' }; // Simple translation questions this._gameContainer.innerHTML = `
-

Practice Phase - ${levelName}

+

Practice Phase (${this._sessionPracticeCount}/${this._cyclePracticeMax} in cycle)

+
+
+
+

Total Progress: ${progress.current} / ${progress.max}

Correct: ${this._practiceCorrect} Total: ${this._practiceTotal} @@ -472,16 +637,6 @@ class WordDiscovery extends Module {
Loading question...
- -
- - -
`; @@ -493,8 +648,7 @@ class WordDiscovery extends Module { const levelConfig = { 0: { options: 4, type: 'translation' }, 1: { options: 3, type: 'mixed' }, - 2: { options: 4, type: 'definition' }, - 3: { options: 4, type: 'context' } + 2: { options: 4, type: 'definition' } }; config = levelConfig[this._currentPracticeLevel]; } @@ -530,9 +684,6 @@ class WordDiscovery extends Module { case 'definition': questionHTML = this._renderDefinitionQuestion(correctWord); break; - case 'context': - questionHTML = this._renderContextQuestion(correctWord); - break; case 'mixed': const types = ['translation', 'definition']; const randomType = types[Math.floor(Math.random() * types.length)]; @@ -541,6 +692,11 @@ class WordDiscovery extends Module { } questionContainer.innerHTML = questionHTML; + + // Auto-play TTS for the target language word in practice mode + setTimeout(() => { + this._playWordSound(correctWord.word); + }, 300); } _renderTranslationQuestion(correctWord) { @@ -586,29 +742,11 @@ class WordDiscovery extends Module { `; } - _renderContextQuestion(correctWord) { - return ` -
-

Complete the sentence:

-
- ${correctWord.example ? correctWord.example.replace(correctWord.word, '_____') : `The _____ is very important.`} -
-
- ${this._practiceOptions.map(option => ` - - `).join('')} -
-
- `; - } - _selectAnswer(selectedWord) { this._practiceTotal++; + this._sessionPracticeCount++; + this._totalPracticeCount++; + const isCorrect = selectedWord === this._correctAnswer.word; if (isCorrect) { @@ -619,8 +757,10 @@ class WordDiscovery extends Module { this._showResult(isCorrect, isCorrect ? 'Correct!' : `Wrong! The answer was: ${this._correctAnswer.word}`); + this._saveSession(); + setTimeout(() => { - this._generateQuestion(); + this._renderCurrentPhase(); }, 1500); } @@ -652,17 +792,44 @@ class WordDiscovery extends Module { } } - _nextLevel() { - if (this._currentPracticeLevel < 3) { - this._currentPracticeLevel++; - this._renderPracticeLevel(); - } + _showCompletionScreen() { + const progress = this._calculateTotalProgress(); + const accuracy = this._practiceTotal > 0 ? Math.round((this._practiceCorrect / this._practiceTotal) * 100) : 0; + + this._gameContainer.innerHTML = ` +
+
+

๐ŸŽ‰ Congratulations!

+

You've completed all vocabulary!

+
+
+ Words Discovered: + ${this._totalDiscoveryCount} +
+
+ Practice Questions: + ${this._totalPracticeCount} +
+
+ Overall Accuracy: + ${accuracy}% +
+
+
+ +
+
+
+ `; } - _backToDiscovery() { - this._currentPhase = 'discovery'; - this._currentWordIndex = 0; - this._renderDiscoveryPhase(); + _restartSession() { + sessionStorage.removeItem('wordDiscovery_session'); + this._startFreshSession(); + this._saveSession(); + this._renderCurrentPhase(); } _injectCSS() { @@ -689,6 +856,13 @@ class WordDiscovery extends Module { margin-bottom: 15px; } + .cycle-info { + font-size: 0.9em; + color: #7f8c8d; + font-style: italic; + margin-top: 5px; + } + .progress-bar { width: 100%; height: 8px; @@ -964,6 +1138,71 @@ class WordDiscovery extends Module { opacity: 0.9; } + .completion-screen { + text-align: center; + padding: 40px 20px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 16px; + color: white; + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); + } + + .completion-screen h2 { + font-size: 2.5em; + margin-bottom: 20px; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); + } + + .completion-stats { + background: rgba(255, 255, 255, 0.1); + border-radius: 12px; + padding: 30px; + margin: 30px 0; + backdrop-filter: blur(10px); + } + + .stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + } + + .stat-item:last-child { + border-bottom: none; + } + + .stat-label { + font-size: 1.2em; + font-weight: 500; + } + + .stat-value { + font-size: 1.5em; + font-weight: bold; + color: #ffd700; + } + + .restart-btn { + padding: 15px 40px; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; + border: none; + border-radius: 50px; + font-size: 1.2em; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + margin-top: 20px; + box-shadow: 0 5px 15px rgba(245, 87, 108, 0.4); + } + + .restart-btn:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px rgba(245, 87, 108, 0.6); + } + @media (max-width: 768px) { .word-discovery-container { padding: 10px; diff --git a/src/games/WordStorm.js b/src/games/WordStorm.js index 3ec7019..41cdda4 100644 --- a/src/games/WordStorm.js +++ b/src/games/WordStorm.js @@ -657,7 +657,7 @@ class WordStorm extends Module {
`; - this._generateAnswerOptions(); + // Answer buttons will be populated when first word spawns } _setupEventListeners() { @@ -718,6 +718,9 @@ class WordStorm extends Module { const word = this._vocabulary[this._currentWordIndex % this._vocabulary.length]; this._currentWordIndex++; + // Generate options for THIS specific word + const options = this._generateOptionsForWord(word); + const gameArea = document.getElementById('game-area'); const wordElement = document.createElement('div'); wordElement.className = 'falling-word'; @@ -734,27 +737,22 @@ class WordStorm extends Module { return; } - const gameArea = document.getElementById('game-area'); - if (!gameArea) { + const answerPanel = document.getElementById('answer-panel'); + if (!answerPanel) { clearInterval(positionCheck); return; } // Get positions using getBoundingClientRect for accuracy const wordRect = wordElement.getBoundingClientRect(); - const gameAreaRect = gameArea.getBoundingClientRect(); + const answerPanelRect = answerPanel.getBoundingClientRect(); - // Calculate word's position relative to game area - const wordTop = wordRect.top; - const wordHeight = wordRect.height; - const gameAreaBottom = gameAreaRect.bottom; + // Calculate word's bottom edge position + const wordBottom = wordRect.bottom; + const panelTop = answerPanelRect.top; - // Destroy when word's bottom edge nears the bottom of the game area - // Use larger margin to ensure word stays visible until destruction - const wordBottom = wordTop + wordHeight; - const threshold = gameAreaBottom - 100; // 100px margin before bottom - - if (wordBottom >= threshold) { + // Destroy when word's bottom edge touches the answer panel + if (wordBottom >= panelTop) { clearInterval(positionCheck); if (wordElement.parentNode) { this._missWord(wordElement); @@ -763,15 +761,18 @@ class WordStorm extends Module { }, 50); // Check every 50ms for smooth detection - this._fallingWords.push({ + const fallingWordData = { element: wordElement, word: word, + options: options, // Store options with this word startTime: Date.now(), positionCheck: positionCheck - }); + }; - // Generate new answer options when word spawns - this._generateAnswerOptions(); + this._fallingWords.push(fallingWordData); + + // Update answer panel with the OLDEST word's options (first word still falling) + this._updateAnswerPanelForOldestWord(); // Animate falling this._animateFalling(wordElement); @@ -797,16 +798,11 @@ class WordStorm extends Module { }, 50); } - _generateAnswerOptions() { - if (this._vocabulary.length === 0) return; - + _generateOptionsForWord(word) { const buttons = []; - const correctWord = this._fallingWords.length > 0 ? - this._fallingWords[this._fallingWords.length - 1].word : - this._vocabulary[0]; // Add correct answer - buttons.push(correctWord.translation); + buttons.push(word.translation); // Add 3 random incorrect answers while (buttons.length < 4) { @@ -816,13 +812,24 @@ class WordStorm extends Module { } } - // Shuffle buttons - this._shuffleArray(buttons); + // Shuffle and return + return this._shuffleArray(buttons); + } + + _updateAnswerPanelForOldestWord() { + // Find the oldest word still falling (first in array) + const activeFallingWords = this._fallingWords.filter(fw => fw.element.parentNode); + + if (activeFallingWords.length === 0) return; + + // Use the OLDEST word's options + const oldestWord = activeFallingWords[0]; + const options = oldestWord.options; // Update answer panel const answerButtons = document.getElementById('answer-buttons'); if (answerButtons) { - answerButtons.innerHTML = buttons.map(answer => + answerButtons.innerHTML = options.map(answer => `` ).join(''); } @@ -892,6 +899,9 @@ class WordStorm extends Module { // Add points popup animation this._showPointsPopup(points, fallingWord.element); + // Update answer panel to show next word's options + this._updateAnswerPanelForOldestWord(); + // Vibration feedback (if supported) if (navigator.vibrate) { navigator.vibrate([50, 30, 50]); @@ -1002,9 +1012,30 @@ class WordStorm extends Module { clearInterval(fallingWord.positionCheck); } - // Remove word + // Play explosion sound + soundSystem.play('enemy_defeat'); + + // EXPLOSION ANIMATION when word reaches bottom if (wordElement.parentNode) { - wordElement.remove(); + wordElement.classList.add('exploding'); + + // Add screen shake effect for life loss + const gameArea = document.getElementById('game-area'); + if (gameArea) { + gameArea.style.animation = 'none'; + gameArea.offsetHeight; // Force reflow + gameArea.style.animation = 'screenShake 0.5s ease-in-out'; + setTimeout(() => { + gameArea.style.animation = ''; + }, 500); + } + + // Remove after explosion animation completes + setTimeout(() => { + if (wordElement.parentNode) { + wordElement.remove(); + } + }, 800); } // Remove from tracking @@ -1016,6 +1047,14 @@ class WordStorm extends Module { this._updateHUD(); + // Update answer panel to show next word's options + this._updateAnswerPanelForOldestWord(); + + // Stronger vibration for life loss + if (navigator.vibrate) { + navigator.vibrate([200, 100, 200, 100, 200]); + } + if (this._lives <= 0) { this._gameOver(); } @@ -1180,7 +1219,6 @@ class WordStorm extends Module { // Update HUD and restart this._updateHUD(); - this._generateAnswerOptions(); this._startSpawning(); }