Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -45208,7 +45208,7 @@ function Home({ onSelect, completedTopics = [], goldMastery = [], coins = 0, isG
))}

{featuredApps.map(app => (
<button key={app.key} onClick={() => { setMenuOpen(false); onSelect(app.key) }} style={{
<button key={app.key} onClick={() => { setMenuOpen(false); setSelectedApp(app) }} style={{
display: 'block', width: '100%', textAlign: 'left', padding: '10px 16px',
background: 'none', border: 'none', cursor: 'pointer', color: 'var(--clr-text)',
fontFamily: 'var(--font-body)', fontSize: '0.95rem', transition: 'background var(--transition)'
Expand Down Expand Up @@ -45292,7 +45292,7 @@ function Home({ onSelect, completedTopics = [], goldMastery = [], coins = 0, isG
const isGold = goldMastery && goldMastery.includes(app.key)
const isCompleted = isStage3Completed(app.key, completedTopics)
return (
<button key={app.key} className={`menu-card ${isGold ? 'gold-card' : app.color}`} onClick={() => onSelect(app.key)}>
<button key={app.key} className={`menu-card ${isGold ? 'gold-card' : app.color}`} onClick={() => setSelectedApp(app)}>
<span className="menu-title">
{app.name}
{isGold && <span className="badge-indicator">🥇</span>}
Expand All @@ -45304,6 +45304,19 @@ function Home({ onSelect, completedTopics = [], goldMastery = [], coins = 0, isG
})}
</div>
<div className="grid-dimension">{rows} × {cols}</div>

{selectedApp && (
<div className="modal-overlay" onClick={() => setSelectedApp(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
<div className="modal-content" onClick={e => e.stopPropagation()} style={{ background: 'var(--bg-primary)', padding: '2rem', borderRadius: '12px', textAlign: 'center', minWidth: '300px' }}>
<h2>{selectedApp.name}</h2>
<p style={{ margin: '1rem 0' }}>How would you like to play?</p>
<div style={{ display: 'flex', gap: '1rem', marginTop: '1.5rem', justifyContent: 'center' }}>
<button className="btn-primary" onClick={() => onSelect(selectedApp.key)}>Single Player</button>
<button className="btn-secondary" style={{ background: 'var(--danger)', color: '#fff' }} onClick={() => onSelectBattle(selectedApp.key)}>Live Battle</button>
</div>
</div>
</div>
)}
</>
)
}
Expand Down
1,195 changes: 202 additions & 993 deletions client/src/BattleApp.jsx

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions client/src/utils/audio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// utils/audio.js

// Lazy-initialize audio context to comply with browser autoplay policies
let audioCtx = null;

function getAudioContext() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
// If the context is suspended (due to autoplay policy), try to resume it
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
return audioCtx;
}

/**
* Play a satisfying high-pitched "ding" for correct answers
*/
export function playCorrectSound() {
try {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();

osc.type = 'sine';
osc.frequency.setValueAtTime(880, ctx.currentTime); // A5
osc.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.1); // Slide up to A6

gain.gain.setValueAtTime(0, ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 0.05);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);

osc.connect(gain);
gain.connect(ctx.destination);

osc.start();
osc.stop(ctx.currentTime + 0.5);
} catch (err) {
console.error('Audio playback failed', err);
}
}

/**
* Play a low buzz for incorrect answers
*/
export function playWrongSound() {
try {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();

osc.type = 'sawtooth';
osc.frequency.setValueAtTime(150, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.3);

gain.gain.setValueAtTime(0, ctx.currentTime);
gain.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 0.05);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);

osc.connect(gain);
gain.connect(ctx.destination);

osc.start();
osc.stop(ctx.currentTime + 0.3);
} catch (err) {
console.error('Audio playback failed', err);
}
}

/**
* Play an ascending fanfare for winning
*/
export function playWinSound() {
try {
const ctx = getAudioContext();
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6

notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();

osc.type = 'triangle';
const startTime = ctx.currentTime + i * 0.15;

osc.frequency.setValueAtTime(freq, startTime);

gain.gain.setValueAtTime(0, startTime);
gain.gain.linearRampToValueAtTime(0.4, startTime + 0.05);
gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.8);

osc.connect(gain);
gain.connect(ctx.destination);

osc.start(startTime);
osc.stop(startTime + 0.8);
});
} catch (err) {
console.error('Audio playback failed', err);
}
}

/**
* Play a descending sad tune for losing
*/
export function playLoseSound() {
try {
const ctx = getAudioContext();
const notes = [392.00, 370.00, 349.23, 329.63]; // G4, Gb4, F4, E4

notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();

osc.type = 'square';
const startTime = ctx.currentTime + i * 0.3;

osc.frequency.setValueAtTime(freq, startTime);

gain.gain.setValueAtTime(0, startTime);
gain.gain.linearRampToValueAtTime(0.2, startTime + 0.1);
gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.6);

osc.connect(gain);
gain.connect(ctx.destination);

osc.start(startTime);
osc.stop(startTime + 0.6);
});
} catch (err) {
console.error('Audio playback failed', err);
}
}
35 changes: 35 additions & 0 deletions client/src/utils/confetti.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import confetti from 'canvas-confetti';

/**
* Triggers a celebratory confetti burst effect.
*/
export function triggerWinConfetti() {
const duration = 3 * 1000;
const animationEnd = Date.now() + duration;
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 10000 };

function randomInRange(min, max) {
return Math.random() * (max - min) + min;
}

const interval = setInterval(function() {
const timeLeft = animationEnd - Date.now();

if (timeLeft <= 0) {
return clearInterval(interval);
}

const particleCount = 50 * (timeLeft / duration);
// since particles fall down, start a bit higher than random
confetti({
...defaults,
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
});
confetti({
...defaults,
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
});
}, 250);
}
1 change: 1 addition & 0 deletions server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const rateLimit = require('express-rate-limit');
const fs = require('fs');
const path = require('path');
const http = require('http');
const { Server } = require('socket.io');
const wordCreator = require('./wordCreator');
const logger = require('./lib/logger');

Expand Down
Loading