feat: add SynthQuest game mode with World 1 (Waves) — 8 puzzle levels

Gamified synth learning inspired by Turing Complete. Progressive puzzle
system teaches oscillators, waveforms, frequency, and mixing through
hands-on module patching. Includes world map, level progression with
3-star rating, target audio playback, solution validation, and
localStorage persistence. Sandbox mode still accessible via button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 02:04:26 +01:00
parent d0755413f3
commit 08206e996e
10 changed files with 1500 additions and 3 deletions

111
src/game/WorldMap.jsx Normal file
View File

@@ -0,0 +1,111 @@
import React from 'react';
import { WORLD_1 } from './levels/world1.js';
import { getLevelProgress, isLevelUnlocked } from './gameState.js';
const worlds = [WORLD_1];
function Stars({ count, max = 3 }) {
return (
<span className="gm-stars">
{Array.from({ length: max }, (_, i) => (
<span key={i} className={i < count ? 'star filled' : 'star empty'}></span>
))}
</span>
);
}
export default function WorldMap({ onSelectLevel, onSandbox }) {
const world = WORLD_1;
const totalStars = world.levels.reduce((s, l) => {
const p = getLevelProgress(l.id);
return s + (p?.stars || 0);
}, 0);
const maxStars = world.levels.length * 3;
return (
<div className="gm-worldmap">
{/* Header */}
<div className="gm-header">
<div className="gm-logo">
<span className="gm-logo-icon">~</span>
<div>
<h1 className="gm-title">SynthQuest</h1>
<p className="gm-tagline">Aprende sintesis modular resolviendo puzzles</p>
</div>
</div>
<div className="gm-header-right">
<div className="gm-total-stars">
<span className="star filled"></span> {totalStars}/{maxStars}
</div>
<button className="gm-sandbox-btn" onClick={onSandbox}>
🎛 Sandbox
</button>
</div>
</div>
{/* World section */}
<div className="gm-world-section">
<div className="gm-world-header">
<span className="gm-world-icon" style={{ color: world.color }}>{world.icon}</span>
<div>
<h2 className="gm-world-name">Mundo 1: {world.name}</h2>
<p className="gm-world-sub">{world.subtitle}</p>
</div>
</div>
{/* Level grid */}
<div className="gm-level-grid">
{world.levels.map((level, idx) => {
const progress = getLevelProgress(level.id);
const unlocked = isLevelUnlocked(level.id, world.levels);
const stars = progress?.stars || 0;
const isBoss = idx === world.levels.length - 1;
return (
<div
key={level.id}
className={`gm-level-card ${unlocked ? 'unlocked' : 'locked'} ${isBoss ? 'boss' : ''} ${stars === 3 ? 'perfect' : ''}`}
onClick={() => unlocked && onSelectLevel(level)}
>
<div className="gm-level-number">{idx + 1}</div>
<div className="gm-level-info">
<h3 className="gm-level-title">{level.title}</h3>
<p className="gm-level-subtitle">{level.subtitle}</p>
</div>
{unlocked ? (
<Stars count={stars} />
) : (
<span className="gm-lock">🔒</span>
)}
{!unlocked && <div className="gm-lock-overlay" />}
</div>
);
})}
</div>
</div>
{/* Future worlds teaser */}
<div className="gm-world-section gm-locked-world">
<div className="gm-world-header">
<span className="gm-world-icon" style={{ color: '#666' }}></span>
<div>
<h2 className="gm-world-name" style={{ color: '#666' }}>Mundo 2: Filtros</h2>
<p className="gm-world-sub">Proximamente... Consigue {Math.ceil(maxStars * 0.6)} estrellas para desbloquear</p>
</div>
<span className="gm-lock" style={{ fontSize: 24 }}>🔒</span>
</div>
</div>
<div className="gm-world-section gm-locked-world">
<div className="gm-world-header">
<span className="gm-world-icon" style={{ color: '#666' }}></span>
<div>
<h2 className="gm-world-name" style={{ color: '#666' }}>Mundo 3: Envelopes</h2>
<p className="gm-world-sub">Proximamente...</p>
</div>
<span className="gm-lock" style={{ fontSize: 24 }}>🔒</span>
</div>
</div>
</div>
);
}