- Admin panel: add/remove stars, unlock worlds, reset progress (🛠 button) - World 4 "Modulación" (8 levels): vibrato, sirena, wah-wah, auto-pan, FM, wobble bass - World 5 "Efectos" (8 levels): delay, slapback, reverb, distortion, dub echo, shoegaze, ambient - World 6 "Diseño Sonoro" (8 levels): kick, hi-hat, snare, pad, reese bass, laser, trance arp, final boss - Star unlock progression: W4=36★, W5=48★, W6=60★ (total 48 levels, 144 stars) - Fix stereo output: left/right channels now route through Tone.Merge for true stereo separation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
5.0 KiB
JavaScript
137 lines
5.0 KiB
JavaScript
import React from 'react';
|
|
import { WORLD_1 } from './levels/world1.js';
|
|
import { WORLD_2 } from './levels/world2.js';
|
|
import { WORLD_3 } from './levels/world3.js';
|
|
import { WORLD_4 } from './levels/world4.js';
|
|
import { WORLD_5 } from './levels/world5.js';
|
|
import { WORLD_6 } from './levels/world6.js';
|
|
import { getLevelProgress, isLevelUnlocked, loadProgress } from './gameState.js';
|
|
|
|
const worlds = [WORLD_1, WORLD_2, WORLD_3, WORLD_4, WORLD_5, WORLD_6];
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
function getTotalStars() {
|
|
const p = loadProgress();
|
|
return Object.values(p.completedLevels).reduce((s, l) => s + (l.stars || 0), 0);
|
|
}
|
|
|
|
function getMaxStars() {
|
|
return worlds.reduce((s, w) => s + w.levels.length * 3, 0);
|
|
}
|
|
|
|
function isWorldUnlocked(world) {
|
|
if (!world.unlockStars) return true; // World 1 always unlocked
|
|
return getTotalStars() >= world.unlockStars;
|
|
}
|
|
|
|
export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
|
|
const totalStars = getTotalStars();
|
|
const maxStars = getMaxStars();
|
|
|
|
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>
|
|
{onAdmin && (
|
|
<button className="gm-admin-btn" onClick={onAdmin} title="Admin Mode">
|
|
🛠
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* All worlds */}
|
|
{worlds.map((world, worldIdx) => {
|
|
const unlocked = isWorldUnlocked(world);
|
|
const worldStars = world.levels.reduce((s, l) => {
|
|
const p = getLevelProgress(l.id);
|
|
return s + (p?.stars || 0);
|
|
}, 0);
|
|
const worldMaxStars = world.levels.length * 3;
|
|
|
|
if (!unlocked) {
|
|
return (
|
|
<div key={world.id} className="gm-world-section gm-locked-world">
|
|
<div className="gm-world-header">
|
|
<span className="gm-world-icon" style={{ color: '#666' }}>{world.icon}</span>
|
|
<div>
|
|
<h2 className="gm-world-name" style={{ color: '#666' }}>Mundo {worldIdx + 1}: {world.name}</h2>
|
|
<p className="gm-world-sub">Consigue {world.unlockStars} estrellas para desbloquear ({totalStars}/{world.unlockStars})</p>
|
|
</div>
|
|
<span className="gm-lock" style={{ fontSize: 24 }}>🔒</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div key={world.id} 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 {worldIdx + 1}: {world.name}</h2>
|
|
<p className="gm-world-sub">{world.subtitle}</p>
|
|
</div>
|
|
<div className="gm-world-stars">
|
|
<span className="star filled">★</span> {worldStars}/{worldMaxStars}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="gm-level-grid">
|
|
{world.levels.map((level, idx) => {
|
|
const progress = getLevelProgress(level.id);
|
|
const levelUnlocked = 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 ${levelUnlocked ? 'unlocked' : 'locked'} ${isBoss ? 'boss' : ''} ${stars === 3 ? 'perfect' : ''}`}
|
|
onClick={() => levelUnlocked && onSelectLevel(level, world)}
|
|
>
|
|
<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>
|
|
{levelUnlocked ? (
|
|
<Stars count={stars} />
|
|
) : (
|
|
<span className="gm-lock">🔒</span>
|
|
)}
|
|
{!levelUnlocked && <div className="gm-lock-overlay" />}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|