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:
StillHammer 2025-10-19 11:14:35 +08:00
parent 8ebc0b2334
commit abb09023dd
7 changed files with 431 additions and 123 deletions

13
TODO.md Normal file
View 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)

View File

@ -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!');
}

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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;
}
}
`;

View File

@ -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;

View File

@ -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);