feat: add Worlds 2-3, patch persistence, and zoom controls

- World 2 (Filtros): 8 levels teaching filters, resonance, LFO modulation, acid bass
- World 3 (Envelopes): 8 levels teaching VCA, ADSR, pluck, tremolo, full synth lead
- Star-based world unlock system (12 stars for W2, 24 for W3)
- Level patch persistence: auto-saves player patches, restores on revisit
- Google Maps-style zoom controls (+/−/reset) in both puzzle and sandbox views
- Multi-world navigation in GameApp and WorldMap
- Target audio now supports filter chain for World 2 levels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Luis
2026-03-21 02:28:36 +01:00
parent 00c4ec8e00
commit 41d993183f
9 changed files with 1292 additions and 95 deletions

View File

@@ -2,43 +2,58 @@ import React, { useState, useCallback } from 'react';
import WorldMap from './WorldMap.jsx';
import PuzzleView from './PuzzleView.jsx';
import { WORLD_1 } from './levels/world1.js';
import { WORLD_2 } from './levels/world2.js';
import { WORLD_3 } from './levels/world3.js';
const allWorlds = [WORLD_1, WORLD_2, WORLD_3];
export default function GameApp({ onSwitchToSandbox }) {
const [view, setView] = useState('map'); // 'map' | 'puzzle'
const [view, setView] = useState('map');
const [currentLevel, setCurrentLevel] = useState(null);
const [currentLevelIndex, setCurrentLevelIndex] = useState(0);
const [currentWorld, setCurrentWorld] = useState(null);
const worldLevels = WORLD_1.levels;
const handleSelectLevel = useCallback((level) => {
const idx = worldLevels.findIndex(l => l.id === level.id);
const handleSelectLevel = useCallback((level, world) => {
const idx = world.levels.findIndex(l => l.id === level.id);
setCurrentLevel(level);
setCurrentLevelIndex(idx);
setCurrentWorld(world);
setView('puzzle');
}, [worldLevels]);
}, []);
const handleBack = useCallback(() => {
setView('map');
setCurrentLevel(null);
setCurrentWorld(null);
}, []);
const handleNextLevel = useCallback(() => {
if (!currentWorld) return;
const nextIdx = currentLevelIndex + 1;
if (nextIdx < worldLevels.length) {
setCurrentLevel(worldLevels[nextIdx]);
if (nextIdx < currentWorld.levels.length) {
setCurrentLevel(currentWorld.levels[nextIdx]);
setCurrentLevelIndex(nextIdx);
} else {
setView('map');
// Move to next world's first level if unlocked
const worldIdx = allWorlds.findIndex(w => w.id === currentWorld.id);
if (worldIdx < allWorlds.length - 1) {
const nextWorld = allWorlds[worldIdx + 1];
setCurrentWorld(nextWorld);
setCurrentLevel(nextWorld.levels[0]);
setCurrentLevelIndex(0);
} else {
setView('map');
}
}
}, [currentLevelIndex, worldLevels]);
}, [currentLevelIndex, currentWorld]);
if (view === 'puzzle' && currentLevel) {
if (view === 'puzzle' && currentLevel && currentWorld) {
return (
<PuzzleView
key={currentLevel.id}
level={currentLevel}
levelIndex={currentLevelIndex}
worldLevels={worldLevels}
worldLevels={currentWorld.levels}
onBack={handleBack}
onNextLevel={handleNextLevel}
/>