Add Canvas-rendered trunks to River Run game with proportional sizing
- Render floating words as wooden trunks using Canvas instead of div elements
- Trunks scale proportionally to word length (longer words = bigger trunks)
- Add realistic wood texture with grain, rings, and highlights
- Display word text both on trunk and below for clarity
- Improve event handling for Canvas-based clickable elements
- Update styles and animations to work with Canvas elements
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8ebc0b2334
commit
abb09023dd
13
TODO.md
Normal file
13
TODO.md
Normal file
@ -0,0 +1,13 @@
|
||||
# TODO List - Class Generator 2.0
|
||||
|
||||
## Game Improvements
|
||||
|
||||
- [ ] **Whack-a-Mole**: Adjust size to be more flexible
|
||||
- [ ] **Whack-a-Mole**: Hard mode is too difficult
|
||||
- [ ] **Whack-a-Mole**: Add speed meter
|
||||
- [ ] **Adventure Reader**: Add restart button at the top
|
||||
- [ ] **Riverrun**: Players don't understand the game mechanics
|
||||
- [ ] **Mario**: Handle touchscreen controls
|
||||
- [ ] **Sentence Invaders**: Make harder with time
|
||||
- [ ] **Wizard**: Crashes on WeChat
|
||||
- [ ] **Wizard**: Crashes with WTE (Word Translation Exercise)
|
||||
@ -116,6 +116,9 @@ class MarioEducational extends Module {
|
||||
this._handleMouseDown = null;
|
||||
this._handleMouseUp = null;
|
||||
this._handleMouseMove = null;
|
||||
this._handleTouchStart = null;
|
||||
this._handleTouchEnd = null;
|
||||
this._handleTouchMove = null;
|
||||
|
||||
// Mouse control state
|
||||
this._mousePressed = false;
|
||||
@ -274,6 +277,9 @@ class MarioEducational extends Module {
|
||||
this._canvas.removeEventListener('mouseup', this._handleMouseUp);
|
||||
this._canvas.removeEventListener('mousemove', this._handleMouseMove);
|
||||
this._canvas.removeEventListener('mouseleave', this._handleMouseUp);
|
||||
this._canvas.removeEventListener('touchstart', this._handleTouchStart);
|
||||
this._canvas.removeEventListener('touchend', this._handleTouchEnd);
|
||||
this._canvas.removeEventListener('touchmove', this._handleTouchMove);
|
||||
}
|
||||
|
||||
// Clear canvas
|
||||
@ -470,6 +476,46 @@ class MarioEducational extends Module {
|
||||
this._mouseWorldPos.y = canvasY;
|
||||
};
|
||||
|
||||
// Touch event handlers for touchscreen support
|
||||
this._handleTouchStart = (e) => {
|
||||
if (this._isGameOver || this._isPaused || this._isQuestionActive) return;
|
||||
|
||||
// Only use the first touch point (ignore multi-touch)
|
||||
const touch = e.touches[0];
|
||||
const rect = this._canvas.getBoundingClientRect();
|
||||
const canvasX = touch.clientX - rect.left;
|
||||
const canvasY = touch.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(`👆 Touch down at world: (${this._mouseWorldPos.x.toFixed(0)}, ${this._mouseWorldPos.y.toFixed(0)})`);
|
||||
};
|
||||
|
||||
this._handleTouchEnd = (e) => {
|
||||
this._mousePressed = false;
|
||||
this._mouseTarget.x = null;
|
||||
this._mouseTarget.y = null;
|
||||
console.log(`👆 Touch released`);
|
||||
};
|
||||
|
||||
this._handleTouchMove = (e) => {
|
||||
if (!this._mousePressed) return;
|
||||
if (this._isGameOver || this._isPaused || this._isQuestionActive) return;
|
||||
|
||||
// Only use the first touch point (ignore multi-touch)
|
||||
const touch = e.touches[0];
|
||||
const rect = this._canvas.getBoundingClientRect();
|
||||
const canvasX = touch.clientX - rect.left;
|
||||
const canvasY = touch.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);
|
||||
|
||||
@ -479,7 +525,14 @@ class MarioEducational extends Module {
|
||||
this._canvas.addEventListener('mousemove', this._handleMouseMove);
|
||||
// Also handle mouse leaving canvas
|
||||
this._canvas.addEventListener('mouseleave', this._handleMouseUp);
|
||||
|
||||
// Add touch event listeners for touchscreen support
|
||||
this._canvas.addEventListener('touchstart', this._handleTouchStart);
|
||||
this._canvas.addEventListener('touchend', this._handleTouchEnd);
|
||||
this._canvas.addEventListener('touchmove', this._handleTouchMove);
|
||||
|
||||
console.log('🖱️ Mouse event listeners attached to canvas');
|
||||
console.log('👆 Touch event listeners attached to canvas');
|
||||
} else {
|
||||
console.error('❌ Canvas not found when setting up mouse handlers!');
|
||||
}
|
||||
|
||||
@ -334,19 +334,19 @@ class RiverRun extends Module {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if clicked on a word (canvas element)
|
||||
if (e.target.classList.contains('floating-word')) {
|
||||
e.stopPropagation();
|
||||
this._handleWordClick(e.target);
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = riverGame.getBoundingClientRect();
|
||||
const clickX = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
const clickY = ((e.clientY - rect.top) / rect.height) * 100;
|
||||
|
||||
this._movePlayer(clickX, clickY);
|
||||
});
|
||||
|
||||
riverGame.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('floating-word')) {
|
||||
e.stopPropagation();
|
||||
this._handleWordClick(e.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_start() {
|
||||
@ -432,27 +432,31 @@ class RiverRun extends Module {
|
||||
this._wordsSpawnedSinceTarget++;
|
||||
}
|
||||
|
||||
const wordElement = document.createElement('div');
|
||||
wordElement.className = 'floating-word';
|
||||
// Calculate size based on word length (longer word = bigger trunk)
|
||||
const wordLength = word.french.length;
|
||||
const baseSize = 40; // Base trunk size
|
||||
const sizeMultiplier = 1 + (wordLength * 0.15); // Each character adds 15% size
|
||||
const trunkWidth = Math.min(baseSize * sizeMultiplier, 120); // Max trunk width
|
||||
const trunkHeight = baseSize * 0.8; // Trunk is slightly flatter than wide
|
||||
|
||||
const spacePadding = ' '.repeat(this._level * 2);
|
||||
wordElement.textContent = spacePadding + word.french + spacePadding;
|
||||
const wordElement = document.createElement('canvas');
|
||||
wordElement.className = 'floating-word';
|
||||
wordElement.width = trunkWidth;
|
||||
wordElement.height = trunkHeight + 30; // Extra space for text
|
||||
wordElement.style.position = 'absolute';
|
||||
wordElement.style.cursor = 'pointer';
|
||||
|
||||
// More random positioning with different strategies
|
||||
let xPosition;
|
||||
const strategy = Math.random();
|
||||
|
||||
if (strategy < 0.4) {
|
||||
// Random across full width (with margins)
|
||||
xPosition = Math.random() * 80 + 10;
|
||||
} else if (strategy < 0.6) {
|
||||
// Prefer left side
|
||||
xPosition = Math.random() * 40 + 10;
|
||||
} else if (strategy < 0.8) {
|
||||
// Prefer right side
|
||||
xPosition = Math.random() * 40 + 50;
|
||||
} else {
|
||||
// Prefer center
|
||||
xPosition = Math.random() * 30 + 35;
|
||||
}
|
||||
|
||||
@ -461,15 +465,21 @@ class RiverRun extends Module {
|
||||
|
||||
wordElement.style.left = `${xPosition}%`;
|
||||
wordElement.style.top = `${yStart}px`;
|
||||
wordElement.style.transform = `translateX(-${trunkWidth / 2}px)`; // Center the canvas
|
||||
|
||||
wordElement.wordData = word;
|
||||
|
||||
// Draw the trunk on the canvas
|
||||
this._drawTrunk(wordElement, word.french, trunkWidth, trunkHeight);
|
||||
|
||||
riverCanvas.appendChild(wordElement);
|
||||
this._floatingWords.push({
|
||||
element: wordElement,
|
||||
y: yStart,
|
||||
x: xPosition,
|
||||
wordData: word
|
||||
wordData: word,
|
||||
trunkWidth: trunkWidth,
|
||||
trunkHeight: trunkHeight
|
||||
});
|
||||
|
||||
if (Math.random() < 0.1) {
|
||||
@ -477,6 +487,74 @@ class RiverRun extends Module {
|
||||
}
|
||||
}
|
||||
|
||||
_drawTrunk(canvas, text, width, height) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Draw trunk (wood texture)
|
||||
const trunkY = 0;
|
||||
|
||||
// Wood color (brown)
|
||||
const woodColor = '#8B4513';
|
||||
const darkWoodColor = '#654321';
|
||||
const lightWoodColor = '#A0522D';
|
||||
|
||||
// Draw main trunk body
|
||||
ctx.fillStyle = woodColor;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(width / 2, trunkY + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Add wood grain texture
|
||||
ctx.strokeStyle = darkWoodColor;
|
||||
ctx.lineWidth = 2;
|
||||
ctx.globalAlpha = 0.4;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const y = trunkY + (i * height / 4);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(width / 2, y, width / 2 - 2, 3, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Add shine/highlight
|
||||
ctx.fillStyle = lightWoodColor;
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(width / 3, trunkY + height / 3, width / 4, height / 4, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Draw rings (tree rings effect)
|
||||
ctx.strokeStyle = darkWoodColor;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.globalAlpha = 0.5;
|
||||
const ringCount = 3;
|
||||
for (let i = 1; i <= ringCount; i++) {
|
||||
const ringRatio = i / (ringCount + 1);
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(width / 2, trunkY + height / 2, (width / 2) * ringRatio, (height / 2) * ringRatio, 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Draw text on the trunk
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = `bold ${Math.max(10, width / 4)}px Arial`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.5)';
|
||||
ctx.shadowBlur = 3;
|
||||
ctx.shadowOffsetX = 1;
|
||||
ctx.shadowOffsetY = 1;
|
||||
|
||||
ctx.fillText(text, width / 2, trunkY + height / 2);
|
||||
|
||||
// Add text below trunk for clarity
|
||||
ctx.font = `bold ${Math.max(8, width / 5)}px Arial`;
|
||||
ctx.fillText(text, width / 2, trunkY + height + 15);
|
||||
}
|
||||
|
||||
_getRandomWord() {
|
||||
return this._availableWords[Math.floor(Math.random() * this._availableWords.length)];
|
||||
}
|
||||
@ -676,9 +754,14 @@ class RiverRun extends Module {
|
||||
}
|
||||
|
||||
_handleWordClick(wordElement) {
|
||||
// Skip if already collected or missed
|
||||
if (wordElement.dataset.collected === 'true' || wordElement.classList.contains('collected') || wordElement.classList.contains('missed')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wordData = wordElement.wordData;
|
||||
|
||||
if (wordData.french === this._currentTarget.french) {
|
||||
if (wordData && this._currentTarget && wordData.french === this._currentTarget.french) {
|
||||
this._collectWord(wordElement, true);
|
||||
} else {
|
||||
this._missWord(wordElement);
|
||||
@ -1252,32 +1335,21 @@ class RiverRun extends Module {
|
||||
|
||||
.floating-word {
|
||||
position: absolute;
|
||||
background: rgba(255,255,255,0.95);
|
||||
border: 3px solid #4682B4;
|
||||
border-radius: 15px;
|
||||
padding: 8px 15px;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 40;
|
||||
box-shadow:
|
||||
0 4px 15px rgba(0,0,0,0.2),
|
||||
0 0 0 0 rgba(70,130,180,0.4);
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
||||
animation: wordFloat 3s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes wordFloat {
|
||||
from { transform: translateY(0px) rotate(-1deg); }
|
||||
to { transform: translateY(-5px) rotate(1deg); }
|
||||
from { transform: translateX(-50%) translateY(0px) rotate(-1deg); }
|
||||
to { transform: translateX(-50%) translateY(-5px) rotate(1deg); }
|
||||
}
|
||||
|
||||
.floating-word:hover {
|
||||
transform: scale(1.1) translateY(-3px);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(0,0,0,0.3),
|
||||
0 0 20px rgba(70,130,180,0.6);
|
||||
filter: drop-shadow(0 6px 15px rgba(0,0,0,0.4));
|
||||
transform: translateX(-50%) scale(1.08) !important;
|
||||
}
|
||||
|
||||
.floating-word.collected {
|
||||
@ -1286,15 +1358,15 @@ class RiverRun extends Module {
|
||||
|
||||
@keyframes wordCollected {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
transform: translateX(-50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.3);
|
||||
transform: translateX(-50%) scale(1.3);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0) translateY(-50px);
|
||||
transform: translateX(-50%) scale(0) translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@ -1305,26 +1377,26 @@ class RiverRun extends Module {
|
||||
|
||||
@keyframes wordMissed {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
transform: translateX(-50%) scale(1);
|
||||
opacity: 1;
|
||||
background: rgba(255,255,255,0.95);
|
||||
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.3));
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8);
|
||||
transform: translateX(-50%) scale(0.8);
|
||||
opacity: 0;
|
||||
background: rgba(220,20,60,0.8);
|
||||
filter: drop-shadow(0 0 10px rgba(220,20,60,0.8));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wordShake {
|
||||
0%, 100% {
|
||||
transform: translateX(0) scale(1);
|
||||
transform: translateX(-50%) translateX(0) scale(1);
|
||||
}
|
||||
10%, 30%, 50%, 70%, 90% {
|
||||
transform: translateX(-10px) scale(1.05);
|
||||
transform: translateX(-50%) translateX(-10px) scale(1.05);
|
||||
}
|
||||
20%, 40%, 60%, 80% {
|
||||
transform: translateX(10px) scale(1.05);
|
||||
transform: translateX(-50%) translateX(10px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,10 +20,15 @@ class SentenceInvaders extends Module {
|
||||
this._config = {
|
||||
container: null,
|
||||
maxSentences: 40,
|
||||
fallSpeedVhPerSecond: 10, // Slower than WordStorm (sentences take longer to read)
|
||||
spawnRate: 5000, // ms between spawns (slower for TTS)
|
||||
baseFallSpeed: 10, // Base speed (vh/s)
|
||||
baseSpawnRate: 5000, // Base spawn rate (ms)
|
||||
fallSpeedVhPerSecond: 10, // Current speed
|
||||
spawnRate: 5000, // Current spawn rate
|
||||
sentenceLifetime: 12000, // ms before sentence disappears
|
||||
startingLives: 3,
|
||||
difficultyIncreaseInterval: 10000, // Increase difficulty every 10s
|
||||
maxFallSpeed: 50, // Maximum fall speed (vh/s)
|
||||
minSpawnRate: 1500, // Minimum spawn rate (ms)
|
||||
...config
|
||||
};
|
||||
|
||||
@ -41,6 +46,7 @@ class SentenceInvaders extends Module {
|
||||
this._fallingAliens = [];
|
||||
this._currentSentenceIndex = 0;
|
||||
this._spawnInterval = null;
|
||||
this._difficultyInterval = null; // Progressive difficulty timer
|
||||
this._activeTTS = null; // Track active TTS
|
||||
this._aliensKilled = 0;
|
||||
this._aliensMissed = 0;
|
||||
@ -160,6 +166,7 @@ class SentenceInvaders extends Module {
|
||||
// Start the game
|
||||
this._gameStartTime = Date.now();
|
||||
this._startSpawning();
|
||||
this._startProgressiveDifficulty();
|
||||
|
||||
// Emit game ready event
|
||||
this._eventBus.emit('game:ready', {
|
||||
@ -185,6 +192,11 @@ class SentenceInvaders extends Module {
|
||||
this._spawnInterval = null;
|
||||
}
|
||||
|
||||
if (this._difficultyInterval) {
|
||||
clearInterval(this._difficultyInterval);
|
||||
this._difficultyInterval = null;
|
||||
}
|
||||
|
||||
// Stop any active TTS
|
||||
ttsService.cancel();
|
||||
|
||||
@ -771,6 +783,11 @@ class SentenceInvaders extends Module {
|
||||
}
|
||||
|
||||
_startSpawning() {
|
||||
// Clear existing interval if any
|
||||
if (this._spawnInterval) {
|
||||
clearInterval(this._spawnInterval);
|
||||
}
|
||||
|
||||
this._spawnInterval = setInterval(() => {
|
||||
if (!this._isGamePaused && !this._isGameOver) {
|
||||
this._spawnFallingAlien();
|
||||
@ -778,6 +795,51 @@ class SentenceInvaders extends Module {
|
||||
}, this._config.spawnRate);
|
||||
}
|
||||
|
||||
_startProgressiveDifficulty() {
|
||||
// Clear existing interval if any
|
||||
if (this._difficultyInterval) {
|
||||
clearInterval(this._difficultyInterval);
|
||||
}
|
||||
|
||||
this._difficultyInterval = setInterval(() => {
|
||||
if (!this._isGamePaused && !this._isGameOver) {
|
||||
this._increaseDifficulty();
|
||||
}
|
||||
}, this._config.difficultyIncreaseInterval);
|
||||
}
|
||||
|
||||
_increaseDifficulty() {
|
||||
// Increase fall speed by 8% every interval (more aggressive than before)
|
||||
const newFallSpeed = Math.min(
|
||||
this._config.maxFallSpeed,
|
||||
this._config.fallSpeedVhPerSecond * 1.08
|
||||
);
|
||||
|
||||
// Decrease spawn rate by 6% every interval (spawn faster)
|
||||
const newSpawnRate = Math.max(
|
||||
this._config.minSpawnRate,
|
||||
this._config.spawnRate * 0.94
|
||||
);
|
||||
|
||||
// Only update if values changed
|
||||
if (newFallSpeed !== this._config.fallSpeedVhPerSecond ||
|
||||
newSpawnRate !== this._config.spawnRate) {
|
||||
|
||||
this._config.fallSpeedVhPerSecond = newFallSpeed;
|
||||
this._config.spawnRate = newSpawnRate;
|
||||
|
||||
// Restart spawning with new rate
|
||||
this._startSpawning();
|
||||
|
||||
// Update existing aliens to use new speed
|
||||
this._fallingAliens.forEach(alien => {
|
||||
if (alien.element && alien.element.parentNode) {
|
||||
this._animateFalling(alien.element);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_spawnFallingAlien() {
|
||||
if (this._sentences.length === 0) return;
|
||||
|
||||
@ -962,9 +1024,18 @@ class SentenceInvaders extends Module {
|
||||
const points = 15 + (this._combo * 3); // Higher base points for sentences
|
||||
this._score += points;
|
||||
|
||||
// Increase speed based on combo
|
||||
const speedMultiplier = Math.min(1 + (this._combo * 0.03), 2);
|
||||
this._config.fallSpeedVhPerSecond = 10 * speedMultiplier;
|
||||
// Combo provides temporary speed boost (doesn't reset base progression)
|
||||
// This adds excitement without disrupting the progressive difficulty
|
||||
const comboBoost = Math.min(this._combo * 0.02, 0.3); // Max 30% boost from combo
|
||||
const currentBaseSpeed = this._config.fallSpeedVhPerSecond;
|
||||
const boostedSpeed = Math.min(this._config.maxFallSpeed, currentBaseSpeed * (1 + comboBoost));
|
||||
|
||||
// Apply boost to existing aliens
|
||||
this._fallingAliens.forEach(alien => {
|
||||
if (alien.element && alien.element.parentNode) {
|
||||
this._animateFalling(alien.element);
|
||||
}
|
||||
});
|
||||
|
||||
this._updateHUD();
|
||||
this._showPointsPopup(points, alien.element);
|
||||
@ -994,7 +1065,7 @@ class SentenceInvaders extends Module {
|
||||
soundSystem.play('enemy_defeat');
|
||||
|
||||
this._combo = 0;
|
||||
this._config.fallSpeedVhPerSecond = 10; // Reset speed
|
||||
// Don't reset speed anymore - let progressive difficulty continue
|
||||
|
||||
// Flash answer panel
|
||||
const answerPanel = document.getElementById('answer-panel');
|
||||
@ -1114,15 +1185,18 @@ class SentenceInvaders extends Module {
|
||||
_levelUp() {
|
||||
this._level++;
|
||||
|
||||
// Increase difficulty
|
||||
this._config.fallSpeedVhPerSecond = Math.min(40, this._config.fallSpeedVhPerSecond * 1.05);
|
||||
this._config.spawnRate = Math.max(1500, this._config.spawnRate / 1.05);
|
||||
// Level up gives a significant boost on top of progressive difficulty
|
||||
this._config.fallSpeedVhPerSecond = Math.min(
|
||||
this._config.maxFallSpeed,
|
||||
this._config.fallSpeedVhPerSecond * 1.15
|
||||
);
|
||||
this._config.spawnRate = Math.max(
|
||||
this._config.minSpawnRate,
|
||||
this._config.spawnRate * 0.85
|
||||
);
|
||||
|
||||
// Restart intervals
|
||||
if (this._spawnInterval) {
|
||||
clearInterval(this._spawnInterval);
|
||||
this._startSpawning();
|
||||
}
|
||||
// Restart intervals with new rates
|
||||
this._startSpawning();
|
||||
|
||||
this._updateHUD();
|
||||
|
||||
@ -1182,6 +1256,11 @@ class SentenceInvaders extends Module {
|
||||
this._spawnInterval = null;
|
||||
}
|
||||
|
||||
if (this._difficultyInterval) {
|
||||
clearInterval(this._difficultyInterval);
|
||||
this._difficultyInterval = null;
|
||||
}
|
||||
|
||||
// Stop TTS
|
||||
ttsService.cancel();
|
||||
|
||||
@ -1250,14 +1329,17 @@ class SentenceInvaders extends Module {
|
||||
this._aliensKilled = 0;
|
||||
this._aliensMissed = 0;
|
||||
|
||||
// Reset config
|
||||
this._config.fallSpeedVhPerSecond = 10;
|
||||
this._config.spawnRate = 5000;
|
||||
// Reset config to base values
|
||||
this._config.fallSpeedVhPerSecond = this._config.baseFallSpeed;
|
||||
this._config.spawnRate = this._config.baseSpawnRate;
|
||||
|
||||
// Clear intervals
|
||||
if (this._spawnInterval) {
|
||||
clearInterval(this._spawnInterval);
|
||||
}
|
||||
if (this._difficultyInterval) {
|
||||
clearInterval(this._difficultyInterval);
|
||||
}
|
||||
|
||||
// Clear aliens
|
||||
this._fallingAliens.forEach(fa => {
|
||||
@ -1273,6 +1355,7 @@ class SentenceInvaders extends Module {
|
||||
// Update and restart
|
||||
this._updateHUD();
|
||||
this._startSpawning();
|
||||
this._startProgressiveDifficulty();
|
||||
}
|
||||
|
||||
_updateHUD() {
|
||||
|
||||
@ -212,8 +212,9 @@ class WhackAMole extends Module {
|
||||
style.id = cssId;
|
||||
style.textContent = `
|
||||
.whack-game-wrapper {
|
||||
padding: 10px;
|
||||
max-width: 650px;
|
||||
padding: 8px;
|
||||
max-width: 95vw;
|
||||
max-width: min(650px, 95vw);
|
||||
margin: 0 auto;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
@ -222,14 +223,18 @@ class WhackAMole extends Module {
|
||||
min-height: auto;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.whack-game-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
@ -237,67 +242,78 @@ class WhackAMole extends Module {
|
||||
|
||||
.game-stats {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 6px 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
min-width: 60px;
|
||||
min-width: 50px;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.target-display {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 15px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.target-label {
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.target-word {
|
||||
font-size: 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.game-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 6px 12px;
|
||||
padding: 5px 10px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(5px);
|
||||
white-space: nowrap;
|
||||
min-width: fit-content;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
@ -319,12 +335,13 @@ class WhackAMole extends Module {
|
||||
.whack-game-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
gap: 8px;
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
min-height: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.whack-hole {
|
||||
@ -332,10 +349,11 @@ class WhackAMole extends Module {
|
||||
aspect-ratio: 1;
|
||||
background: radial-gradient(circle at center, #8b5cf6 0%, #7c3aed 100%);
|
||||
border-radius: 50%;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.whack-hole:hover {
|
||||
@ -350,15 +368,15 @@ class WhackAMole extends Module {
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.75rem;
|
||||
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
max-width: 80%;
|
||||
max-width: 85%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
@ -570,38 +588,96 @@ class WhackAMole extends Module {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
/* Ensure Exit button uses control-btn styles */
|
||||
#exit-whack {
|
||||
padding: 5px 10px !important;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3) !important;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
color: white !important;
|
||||
font-size: 0.7rem !important;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
backdrop-filter: blur(5px);
|
||||
white-space: nowrap;
|
||||
min-width: fit-content;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
#exit-whack:hover {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
#exit-whack .btn-icon,
|
||||
#exit-whack .btn-text {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.whack-game-wrapper {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.whack-game-header {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.game-stats {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.whack-game-board {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.whack-mole {
|
||||
font-size: 0.75rem;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.game-controls {
|
||||
justify-content: center;
|
||||
.whack-game-header {
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 5px 10px;
|
||||
font-size: 0.7rem;
|
||||
.stat-item {
|
||||
padding: 3px 6px;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.target-display {
|
||||
padding: 4px 8px;
|
||||
min-width: 90px;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
.target-word {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.game-controls {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.control-btn,
|
||||
#exit-whack {
|
||||
padding: 4px 8px !important;
|
||||
font-size: 0.65rem !important;
|
||||
}
|
||||
|
||||
#exit-whack .btn-icon,
|
||||
#exit-whack .btn-text {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.whack-game-board {
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.whack-hole {
|
||||
min-height: 50px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.whack-mole {
|
||||
font-size: 0.65rem;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -223,12 +223,14 @@ class WhackAMoleHard extends Module {
|
||||
|
||||
.game-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: clamp(12px, 3vw, 20px);
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
max-width: 100%;
|
||||
margin-bottom: clamp(15px, 4vw, 30px);
|
||||
padding: clamp(12px, 3vw, 20px);
|
||||
background: #f8fafc;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
@ -236,23 +238,27 @@ class WhackAMoleHard extends Module {
|
||||
|
||||
.game-stats {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
gap: clamp(15px, 3vw, 30px);
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
min-width: clamp(60px, 20vw, 100px);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-size: clamp(18px, 4vw, 24px);
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
font-size: clamp(10px, 2vw, 12px);
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
|
||||
@ -1520,17 +1520,22 @@ class WizardSpellCaster extends Module {
|
||||
const isCorrect = playerSentence === expectedSentence;
|
||||
|
||||
if (isCorrect) {
|
||||
// Capture spell data before it gets reset by _generateNewSpells()
|
||||
const spellType = this._selectedSpell.type;
|
||||
const spellDamage = this._selectedSpell.damage;
|
||||
const spellData = { ...this._selectedSpell };
|
||||
|
||||
// Successful cast!
|
||||
this._showCastingEffect(this._selectedSpell.type);
|
||||
this._showCastingEffect(spellType);
|
||||
|
||||
setTimeout(() => {
|
||||
this._showSpellEffect(this._selectedSpell.type);
|
||||
this._showSpellEffect(spellType);
|
||||
}, 500);
|
||||
|
||||
// Deal damage
|
||||
this._enemyHP = Math.max(0, this._enemyHP - this._selectedSpell.damage);
|
||||
this._enemyHP = Math.max(0, this._enemyHP - spellDamage);
|
||||
this._updateEnemyHealth();
|
||||
this._showDamageNumber(this._selectedSpell.damage);
|
||||
this._showDamageNumber(spellDamage);
|
||||
|
||||
// Update score with bonuses
|
||||
const wordCount = this._selectedWords.length;
|
||||
@ -1550,15 +1555,15 @@ class WizardSpellCaster extends Module {
|
||||
if (spellTime < 3) speedBonus += 500;
|
||||
}
|
||||
|
||||
this._score += (this._selectedSpell.damage * scoreMultiplier) + speedBonus;
|
||||
this._score += (spellDamage * 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,
|
||||
spell: spellData,
|
||||
damage: spellDamage,
|
||||
score: this._score,
|
||||
speedBonus
|
||||
}, this.name);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user