asemxin commited on
Commit
4539eae
·
1 Parent(s): 2d54981

Add multiple levels and sound effects system

Browse files
app/components/GameCanvas.tsx CHANGED
@@ -1,30 +1,35 @@
1
  'use client';
2
 
3
- import { useEffect, useRef } from 'react';
4
  import { Game } from '@/lib/engine/Game';
5
  import { GAME_CONFIG } from '@/lib/constants';
6
 
7
  interface GameCanvasProps {
8
  onGameOver: (isWin: boolean, score: number) => void;
 
9
  }
10
 
11
- export default function GameCanvas({ onGameOver }: GameCanvasProps) {
12
  const canvasRef = useRef<HTMLCanvasElement>(null);
13
  const gameRef = useRef<Game | null>(null);
14
 
 
 
 
 
15
  useEffect(() => {
16
  if (!canvasRef.current) return;
17
 
18
- const game = new Game(canvasRef.current);
19
  gameRef.current = game;
20
 
21
- game.setOnGameOver(onGameOver);
22
  game.start();
23
 
24
  return () => {
25
  game.stop();
26
  };
27
- }, [onGameOver]);
28
 
29
  return (
30
  <canvas
 
1
  'use client';
2
 
3
+ import { useEffect, useRef, useCallback } from 'react';
4
  import { Game } from '@/lib/engine/Game';
5
  import { GAME_CONFIG } from '@/lib/constants';
6
 
7
  interface GameCanvasProps {
8
  onGameOver: (isWin: boolean, score: number) => void;
9
+ startLevel?: number;
10
  }
11
 
12
+ export default function GameCanvas({ onGameOver, startLevel = 0 }: GameCanvasProps) {
13
  const canvasRef = useRef<HTMLCanvasElement>(null);
14
  const gameRef = useRef<Game | null>(null);
15
 
16
+ const handleGameOver = useCallback((isWin: boolean, score: number) => {
17
+ onGameOver(isWin, score);
18
+ }, [onGameOver]);
19
+
20
  useEffect(() => {
21
  if (!canvasRef.current) return;
22
 
23
+ const game = new Game(canvasRef.current, startLevel);
24
  gameRef.current = game;
25
 
26
+ game.setOnGameOver(handleGameOver);
27
  game.start();
28
 
29
  return () => {
30
  game.stop();
31
  };
32
+ }, [handleGameOver, startLevel]);
33
 
34
  return (
35
  <canvas
app/components/StartScreen.tsx CHANGED
@@ -1,10 +1,22 @@
1
  'use client';
2
 
 
 
 
3
  interface StartScreenProps {
4
- onStart: () => void;
5
  }
6
 
7
  export default function StartScreen({ onStart }: StartScreenProps) {
 
 
 
 
 
 
 
 
 
8
  return (
9
  <div className="start-screen">
10
  <div className="title-container">
@@ -13,7 +25,19 @@ export default function StartScreen({ onStart }: StartScreenProps) {
13
  <p className="subtitle">WEB EDITION</p>
14
  </div>
15
 
16
- <button className="start-button" onClick={onStart}>
 
 
 
 
 
 
 
 
 
 
 
 
17
  START GAME
18
  </button>
19
 
 
1
  'use client';
2
 
3
+ import { getAudioManager } from '@/lib/engine/AudioManager';
4
+ import { LevelManager } from '@/lib/engine/LevelManager';
5
+
6
  interface StartScreenProps {
7
+ onStart: (levelIndex?: number) => void;
8
  }
9
 
10
  export default function StartScreen({ onStart }: StartScreenProps) {
11
+ const levelManager = new LevelManager();
12
+ const levels = levelManager.getAllLevels();
13
+
14
+ const handleToggleSound = () => {
15
+ const audioManager = getAudioManager();
16
+ audioManager.toggleMute();
17
+ // Force re-render would be needed for UI update
18
+ };
19
+
20
  return (
21
  <div className="start-screen">
22
  <div className="title-container">
 
25
  <p className="subtitle">WEB EDITION</p>
26
  </div>
27
 
28
+ <div className="level-buttons">
29
+ {levels.map((level, index) => (
30
+ <button
31
+ key={level.id}
32
+ className={`level-button ${level.theme}`}
33
+ onClick={() => onStart(index)}
34
+ >
35
+ {level.name}
36
+ </button>
37
+ ))}
38
+ </div>
39
+
40
+ <button className="start-button" onClick={() => onStart(0)}>
41
  START GAME
42
  </button>
43
 
app/globals.css CHANGED
@@ -39,15 +39,23 @@ body {
39
  }
40
 
41
  @keyframes blink {
42
- 0%, 50% { opacity: 1; }
43
- 51%, 100% { opacity: 0; }
 
 
 
 
 
 
 
 
44
  }
45
 
46
  /* Game Canvas */
47
  .game-canvas {
48
  border: 4px solid #333;
49
  border-radius: 8px;
50
- box-shadow:
51
  0 0 0 4px var(--mario-red),
52
  0 0 30px rgba(229, 37, 33, 0.3),
53
  0 20px 60px rgba(0, 0, 0, 0.5);
@@ -66,7 +74,7 @@ body {
66
  background: linear-gradient(180deg, var(--sky-blue) 0%, #87ceeb 60%, var(--mario-green) 60%, #2d5a27 100%);
67
  border: 4px solid #333;
68
  border-radius: 8px;
69
- box-shadow:
70
  0 0 0 4px var(--mario-red),
71
  0 0 30px rgba(229, 37, 33, 0.3),
72
  0 20px 60px rgba(0, 0, 0, 0.5);
@@ -83,7 +91,7 @@ body {
83
  height: 40px;
84
  background: white;
85
  border-radius: 50%;
86
- box-shadow:
87
  100px 20px 0 30px white,
88
  250px -10px 0 20px white,
89
  400px 10px 0 25px white;
@@ -99,7 +107,7 @@ body {
99
  .game-title {
100
  font-size: 48px;
101
  color: white;
102
- text-shadow:
103
  4px 4px 0 #000,
104
  -2px -2px 0 #000,
105
  2px -2px 0 #000,
@@ -115,8 +123,13 @@ body {
115
  }
116
 
117
  @keyframes bounce {
118
- from { transform: translateY(0); }
119
- to { transform: translateY(-8px); }
 
 
 
 
 
120
  }
121
 
122
  .subtitle {
@@ -127,6 +140,49 @@ body {
127
  letter-spacing: 6px;
128
  }
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  .start-button {
131
  font-family: 'Press Start 2P', cursive;
132
  font-size: 18px;
@@ -137,7 +193,7 @@ body {
137
  border-radius: 8px;
138
  cursor: pointer;
139
  text-shadow: 2px 2px 0 #000;
140
- box-shadow:
141
  0 6px 0 #8b0000,
142
  0 8px 20px rgba(0, 0, 0, 0.4);
143
  transition: all 0.1s ease;
@@ -146,14 +202,14 @@ body {
146
 
147
  .start-button:hover {
148
  transform: translateY(-2px);
149
- box-shadow:
150
  0 8px 0 #8b0000,
151
  0 10px 25px rgba(0, 0, 0, 0.5);
152
  }
153
 
154
  .start-button:active {
155
  transform: translateY(4px);
156
- box-shadow:
157
  0 2px 0 #8b0000,
158
  0 4px 10px rgba(0, 0, 0, 0.4);
159
  }
@@ -209,7 +265,7 @@ body {
209
  height: 480px;
210
  border: 4px solid #333;
211
  border-radius: 8px;
212
- box-shadow:
213
  0 0 0 4px var(--mario-red),
214
  0 0 30px rgba(229, 37, 33, 0.3),
215
  0 20px 60px rgba(0, 0, 0, 0.5);
@@ -230,7 +286,7 @@ body {
230
  .result-title {
231
  font-size: 32px;
232
  color: white;
233
- text-shadow:
234
  4px 4px 0 #000,
235
  -2px -2px 0 #000;
236
  margin-bottom: 30px;
@@ -275,7 +331,7 @@ body {
275
  border-radius: 8px;
276
  cursor: pointer;
277
  text-shadow: 2px 2px 0 #000;
278
- box-shadow:
279
  0 6px 0 #025a7c,
280
  0 8px 20px rgba(0, 0, 0, 0.4);
281
  transition: all 0.1s ease;
@@ -284,20 +340,21 @@ body {
284
 
285
  .restart-button:hover {
286
  transform: translateY(-2px);
287
- box-shadow:
288
  0 8px 0 #025a7c,
289
  0 10px 25px rgba(0, 0, 0, 0.5);
290
  }
291
 
292
  .restart-button:active {
293
  transform: translateY(4px);
294
- box-shadow:
295
  0 2px 0 #025a7c,
296
  0 4px 10px rgba(0, 0, 0, 0.4);
297
  }
298
 
299
  /* Responsive */
300
  @media (max-width: 850px) {
 
301
  .game-canvas,
302
  .start-screen,
303
  .game-over-screen {
@@ -306,12 +363,12 @@ body {
306
  height: auto;
307
  aspect-ratio: 800 / 480;
308
  }
309
-
310
  .game-title {
311
  font-size: 32px;
312
  }
313
-
314
  .game-title.mario {
315
  font-size: 40px;
316
  }
317
- }
 
39
  }
40
 
41
  @keyframes blink {
42
+
43
+ 0%,
44
+ 50% {
45
+ opacity: 1;
46
+ }
47
+
48
+ 51%,
49
+ 100% {
50
+ opacity: 0;
51
+ }
52
  }
53
 
54
  /* Game Canvas */
55
  .game-canvas {
56
  border: 4px solid #333;
57
  border-radius: 8px;
58
+ box-shadow:
59
  0 0 0 4px var(--mario-red),
60
  0 0 30px rgba(229, 37, 33, 0.3),
61
  0 20px 60px rgba(0, 0, 0, 0.5);
 
74
  background: linear-gradient(180deg, var(--sky-blue) 0%, #87ceeb 60%, var(--mario-green) 60%, #2d5a27 100%);
75
  border: 4px solid #333;
76
  border-radius: 8px;
77
+ box-shadow:
78
  0 0 0 4px var(--mario-red),
79
  0 0 30px rgba(229, 37, 33, 0.3),
80
  0 20px 60px rgba(0, 0, 0, 0.5);
 
91
  height: 40px;
92
  background: white;
93
  border-radius: 50%;
94
+ box-shadow:
95
  100px 20px 0 30px white,
96
  250px -10px 0 20px white,
97
  400px 10px 0 25px white;
 
107
  .game-title {
108
  font-size: 48px;
109
  color: white;
110
+ text-shadow:
111
  4px 4px 0 #000,
112
  -2px -2px 0 #000,
113
  2px -2px 0 #000,
 
123
  }
124
 
125
  @keyframes bounce {
126
+ from {
127
+ transform: translateY(0);
128
+ }
129
+
130
+ to {
131
+ transform: translateY(-8px);
132
+ }
133
  }
134
 
135
  .subtitle {
 
140
  letter-spacing: 6px;
141
  }
142
 
143
+ /* Level Selection */
144
+ .level-buttons {
145
+ display: flex;
146
+ gap: 10px;
147
+ margin-bottom: 15px;
148
+ z-index: 1;
149
+ }
150
+
151
+ .level-button {
152
+ font-family: 'Press Start 2P', cursive;
153
+ font-size: 10px;
154
+ padding: 10px 15px;
155
+ color: white;
156
+ border: none;
157
+ border-radius: 6px;
158
+ cursor: pointer;
159
+ text-shadow: 1px 1px 0 #000;
160
+ transition: all 0.1s ease;
161
+ }
162
+
163
+ .level-button.overworld {
164
+ background: linear-gradient(180deg, var(--sky-blue) 0%, #4a7acc 100%);
165
+ box-shadow: 0 4px 0 #3a5a9c;
166
+ }
167
+
168
+ .level-button.underground {
169
+ background: linear-gradient(180deg, #4a4a6a 0%, #2a2a4a 100%);
170
+ box-shadow: 0 4px 0 #1a1a3a;
171
+ }
172
+
173
+ .level-button.castle {
174
+ background: linear-gradient(180deg, #8b3030 0%, #5a2020 100%);
175
+ box-shadow: 0 4px 0 #3a1010;
176
+ }
177
+
178
+ .level-button:hover {
179
+ transform: translateY(-2px);
180
+ }
181
+
182
+ .level-button:active {
183
+ transform: translateY(2px);
184
+ }
185
+
186
  .start-button {
187
  font-family: 'Press Start 2P', cursive;
188
  font-size: 18px;
 
193
  border-radius: 8px;
194
  cursor: pointer;
195
  text-shadow: 2px 2px 0 #000;
196
+ box-shadow:
197
  0 6px 0 #8b0000,
198
  0 8px 20px rgba(0, 0, 0, 0.4);
199
  transition: all 0.1s ease;
 
202
 
203
  .start-button:hover {
204
  transform: translateY(-2px);
205
+ box-shadow:
206
  0 8px 0 #8b0000,
207
  0 10px 25px rgba(0, 0, 0, 0.5);
208
  }
209
 
210
  .start-button:active {
211
  transform: translateY(4px);
212
+ box-shadow:
213
  0 2px 0 #8b0000,
214
  0 4px 10px rgba(0, 0, 0, 0.4);
215
  }
 
265
  height: 480px;
266
  border: 4px solid #333;
267
  border-radius: 8px;
268
+ box-shadow:
269
  0 0 0 4px var(--mario-red),
270
  0 0 30px rgba(229, 37, 33, 0.3),
271
  0 20px 60px rgba(0, 0, 0, 0.5);
 
286
  .result-title {
287
  font-size: 32px;
288
  color: white;
289
+ text-shadow:
290
  4px 4px 0 #000,
291
  -2px -2px 0 #000;
292
  margin-bottom: 30px;
 
331
  border-radius: 8px;
332
  cursor: pointer;
333
  text-shadow: 2px 2px 0 #000;
334
+ box-shadow:
335
  0 6px 0 #025a7c,
336
  0 8px 20px rgba(0, 0, 0, 0.4);
337
  transition: all 0.1s ease;
 
340
 
341
  .restart-button:hover {
342
  transform: translateY(-2px);
343
+ box-shadow:
344
  0 8px 0 #025a7c,
345
  0 10px 25px rgba(0, 0, 0, 0.5);
346
  }
347
 
348
  .restart-button:active {
349
  transform: translateY(4px);
350
+ box-shadow:
351
  0 2px 0 #025a7c,
352
  0 4px 10px rgba(0, 0, 0, 0.4);
353
  }
354
 
355
  /* Responsive */
356
  @media (max-width: 850px) {
357
+
358
  .game-canvas,
359
  .start-screen,
360
  .game-over-screen {
 
363
  height: auto;
364
  aspect-ratio: 800 / 480;
365
  }
366
+
367
  .game-title {
368
  font-size: 32px;
369
  }
370
+
371
  .game-title.mario {
372
  font-size: 40px;
373
  }
374
+ }
app/page.tsx CHANGED
@@ -17,8 +17,10 @@ export default function Home() {
17
  const [currentScreen, setCurrentScreen] = useState<GameScreen>('start');
18
  const [isWin, setIsWin] = useState(false);
19
  const [finalScore, setFinalScore] = useState(0);
 
20
 
21
- const handleStart = useCallback(() => {
 
22
  setCurrentScreen('playing');
23
  }, []);
24
 
@@ -39,7 +41,11 @@ export default function Home() {
39
  )}
40
 
41
  {currentScreen === 'playing' && (
42
- <GameCanvas onGameOver={handleGameOver} />
 
 
 
 
43
  )}
44
 
45
  {currentScreen === 'gameOver' && (
 
17
  const [currentScreen, setCurrentScreen] = useState<GameScreen>('start');
18
  const [isWin, setIsWin] = useState(false);
19
  const [finalScore, setFinalScore] = useState(0);
20
+ const [startLevel, setStartLevel] = useState(0);
21
 
22
+ const handleStart = useCallback((levelIndex: number = 0) => {
23
+ setStartLevel(levelIndex);
24
  setCurrentScreen('playing');
25
  }, []);
26
 
 
41
  )}
42
 
43
  {currentScreen === 'playing' && (
44
+ <GameCanvas
45
+ key={startLevel}
46
+ onGameOver={handleGameOver}
47
+ startLevel={startLevel}
48
+ />
49
  )}
50
 
51
  {currentScreen === 'gameOver' && (
lib/constants.ts CHANGED
@@ -40,7 +40,7 @@ export const GAME_CONFIG = {
40
  } as const;
41
 
42
  export const KEYS = {
43
- LEFT: ['ArrowLeft', 'KeyA'],
44
- RIGHT: ['ArrowRight', 'KeyD'],
45
- JUMP: ['ArrowUp', 'KeyW', 'Space'],
46
- } as const;
 
40
  } as const;
41
 
42
  export const KEYS = {
43
+ LEFT: ['ArrowLeft', 'KeyA'] as string[],
44
+ RIGHT: ['ArrowRight', 'KeyD'] as string[],
45
+ JUMP: ['ArrowUp', 'KeyW', 'Space'] as string[],
46
+ };
lib/engine/AudioManager.ts ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Audio Manager - handles all game sounds and music
2
+
3
+ export type SoundEffect = 'jump' | 'coin' | 'stomp' | 'death' | 'levelup' | 'gameover';
4
+
5
+ export class AudioManager {
6
+ private sounds: Map<string, HTMLAudioElement> = new Map();
7
+ private bgm: HTMLAudioElement | null = null;
8
+ private isMuted: boolean = false;
9
+ private volume: number = 0.5;
10
+ private initialized: boolean = false;
11
+
12
+ constructor() {
13
+ if (typeof window !== 'undefined') {
14
+ this.initSounds();
15
+ }
16
+ }
17
+
18
+ private initSounds(): void {
19
+ // Create audio elements with Web Audio API fallback
20
+ const soundEffects: SoundEffect[] = ['jump', 'coin', 'stomp', 'death', 'levelup', 'gameover'];
21
+
22
+ soundEffects.forEach(sound => {
23
+ const audio = new Audio();
24
+ audio.volume = this.volume;
25
+ // Use data URLs for simple 8-bit sounds
26
+ audio.src = this.getDataUrl(sound);
27
+ audio.preload = 'auto';
28
+ this.sounds.set(sound, audio);
29
+ });
30
+
31
+ // Background music
32
+ this.bgm = new Audio();
33
+ this.bgm.volume = this.volume * 0.3;
34
+ this.bgm.loop = true;
35
+
36
+ this.initialized = true;
37
+ }
38
+
39
+ private getDataUrl(sound: SoundEffect): string {
40
+ // Generate simple 8-bit style sounds using oscillator tones encoded as data URLs
41
+ // These are placeholder audio - in production, use actual audio files
42
+ const ctx = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
43
+
44
+ const frequencies: Record<SoundEffect, { freq: number; duration: number; type: OscillatorType }> = {
45
+ jump: { freq: 400, duration: 0.15, type: 'square' },
46
+ coin: { freq: 800, duration: 0.1, type: 'square' },
47
+ stomp: { freq: 200, duration: 0.1, type: 'square' },
48
+ death: { freq: 150, duration: 0.5, type: 'sawtooth' },
49
+ levelup: { freq: 600, duration: 0.3, type: 'square' },
50
+ gameover: { freq: 100, duration: 0.8, type: 'sawtooth' },
51
+ };
52
+
53
+ // Store context for later use
54
+ this.audioContext = ctx;
55
+ this.soundConfigs = frequencies;
56
+
57
+ // Return empty data URL - we'll use Web Audio API directly
58
+ return 'data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA=';
59
+ }
60
+
61
+ private audioContext: AudioContext | null = null;
62
+ private soundConfigs: Record<SoundEffect, { freq: number; duration: number; type: OscillatorType }> | null = null;
63
+
64
+ play(sound: SoundEffect): void {
65
+ if (this.isMuted || !this.initialized) return;
66
+
67
+ // Use Web Audio API for dynamic sound generation
68
+ if (this.audioContext && this.soundConfigs) {
69
+ try {
70
+ const config = this.soundConfigs[sound];
71
+ const oscillator = this.audioContext.createOscillator();
72
+ const gainNode = this.audioContext.createGain();
73
+
74
+ oscillator.type = config.type;
75
+ oscillator.frequency.setValueAtTime(config.freq, this.audioContext.currentTime);
76
+
77
+ // Frequency slide for jump sound
78
+ if (sound === 'jump') {
79
+ oscillator.frequency.exponentialRampToValueAtTime(
80
+ config.freq * 2,
81
+ this.audioContext.currentTime + config.duration
82
+ );
83
+ }
84
+
85
+ // Frequency drop for death sound
86
+ if (sound === 'death' || sound === 'gameover') {
87
+ oscillator.frequency.exponentialRampToValueAtTime(
88
+ config.freq * 0.5,
89
+ this.audioContext.currentTime + config.duration
90
+ );
91
+ }
92
+
93
+ // Coin sound - quick double beep
94
+ if (sound === 'coin') {
95
+ oscillator.frequency.setValueAtTime(config.freq, this.audioContext.currentTime);
96
+ oscillator.frequency.setValueAtTime(config.freq * 1.5, this.audioContext.currentTime + 0.05);
97
+ }
98
+
99
+ gainNode.gain.setValueAtTime(this.volume * 0.3, this.audioContext.currentTime);
100
+ gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + config.duration);
101
+
102
+ oscillator.connect(gainNode);
103
+ gainNode.connect(this.audioContext.destination);
104
+
105
+ oscillator.start(this.audioContext.currentTime);
106
+ oscillator.stop(this.audioContext.currentTime + config.duration);
107
+ } catch (e) {
108
+ console.warn('Audio playback failed:', e);
109
+ }
110
+ }
111
+ }
112
+
113
+ playBGM(): void {
114
+ if (this.isMuted || !this.bgm) return;
115
+
116
+ // Create a simple looping melody using Web Audio API
117
+ if (this.audioContext) {
118
+ this.createBGMLoop();
119
+ }
120
+ }
121
+
122
+ private bgmInterval: ReturnType<typeof setInterval> | null = null;
123
+
124
+ private createBGMLoop(): void {
125
+ if (!this.audioContext || this.bgmInterval) return;
126
+
127
+ const notes = [262, 294, 330, 349, 392, 440, 494, 523]; // C major scale
128
+ let noteIndex = 0;
129
+
130
+ this.bgmInterval = setInterval(() => {
131
+ if (this.isMuted || !this.audioContext) {
132
+ if (this.bgmInterval) clearInterval(this.bgmInterval);
133
+ return;
134
+ }
135
+
136
+ const oscillator = this.audioContext.createOscillator();
137
+ const gainNode = this.audioContext.createGain();
138
+
139
+ oscillator.type = 'square';
140
+ oscillator.frequency.setValueAtTime(notes[noteIndex % notes.length], this.audioContext.currentTime);
141
+
142
+ gainNode.gain.setValueAtTime(this.volume * 0.1, this.audioContext.currentTime);
143
+ gainNode.gain.exponentialRampToValueAtTime(0.01, this.audioContext.currentTime + 0.15);
144
+
145
+ oscillator.connect(gainNode);
146
+ gainNode.connect(this.audioContext.destination);
147
+
148
+ oscillator.start(this.audioContext.currentTime);
149
+ oscillator.stop(this.audioContext.currentTime + 0.15);
150
+
151
+ noteIndex++;
152
+ }, 200);
153
+ }
154
+
155
+ stopBGM(): void {
156
+ if (this.bgmInterval) {
157
+ clearInterval(this.bgmInterval);
158
+ this.bgmInterval = null;
159
+ }
160
+ if (this.bgm) {
161
+ this.bgm.pause();
162
+ this.bgm.currentTime = 0;
163
+ }
164
+ }
165
+
166
+ setVolume(volume: number): void {
167
+ this.volume = Math.max(0, Math.min(1, volume));
168
+
169
+ this.sounds.forEach(audio => {
170
+ audio.volume = this.volume;
171
+ });
172
+
173
+ if (this.bgm) {
174
+ this.bgm.volume = this.volume * 0.3;
175
+ }
176
+ }
177
+
178
+ getVolume(): number {
179
+ return this.volume;
180
+ }
181
+
182
+ toggleMute(): boolean {
183
+ this.isMuted = !this.isMuted;
184
+
185
+ if (this.isMuted) {
186
+ this.stopBGM();
187
+ }
188
+
189
+ return this.isMuted;
190
+ }
191
+
192
+ isSoundMuted(): boolean {
193
+ return this.isMuted;
194
+ }
195
+
196
+ setMuted(muted: boolean): void {
197
+ this.isMuted = muted;
198
+ if (muted) {
199
+ this.stopBGM();
200
+ }
201
+ }
202
+
203
+ // Resume audio context (needed after user interaction)
204
+ resume(): void {
205
+ if (this.audioContext && this.audioContext.state === 'suspended') {
206
+ this.audioContext.resume();
207
+ }
208
+ }
209
+ }
210
+
211
+ // Singleton instance
212
+ let audioManagerInstance: AudioManager | null = null;
213
+
214
+ export function getAudioManager(): AudioManager {
215
+ if (!audioManagerInstance) {
216
+ audioManagerInstance = new AudioManager();
217
+ }
218
+ return audioManagerInstance;
219
+ }
lib/engine/Game.ts CHANGED
@@ -4,36 +4,53 @@ import { GameState, KeyState, PlayerState, Enemy, Block } from './types';
4
  import { Renderer } from './Renderer';
5
  import { Physics } from './Physics';
6
  import { Collision } from './Collision';
7
- import { createLevel1 } from './Level';
 
8
  import { GAME_CONFIG, KEYS } from '../constants';
9
 
10
  const { CANVAS_WIDTH, CANVAS_HEIGHT, LEVEL_HEIGHT, PLAYER_WIDTH, PLAYER_HEIGHT } = GAME_CONFIG;
11
 
 
 
 
 
 
 
12
  export class Game {
13
  private canvas: HTMLCanvasElement;
14
  private ctx: CanvasRenderingContext2D;
15
  private renderer: Renderer;
16
  private physics: Physics;
17
  private collision: Collision;
 
18
  private state: GameState;
19
  private keys: KeyState;
20
  private animationId: number | null = null;
21
- private onGameOver?: (isWin: boolean, score: number) => void;
 
 
22
 
23
- constructor(canvas: HTMLCanvasElement) {
24
  this.canvas = canvas;
25
  this.ctx = canvas.getContext('2d')!;
26
  this.renderer = new Renderer(this.ctx);
27
  this.physics = new Physics();
28
  this.collision = new Collision();
 
29
  this.keys = { left: false, right: false, jump: false };
30
- this.state = this.createInitialState();
31
 
 
 
 
 
 
 
 
32
  this.setupEventListeners();
33
  }
34
 
35
  private createInitialState(): GameState {
36
- const level = createLevel1();
37
 
38
  return {
39
  player: {
@@ -47,7 +64,7 @@ export class Game {
47
  isFalling: true,
48
  facingRight: true,
49
  isAlive: true,
50
- score: 0,
51
  coins: 0,
52
  },
53
  enemies: level.enemies.map(e => ({ ...e })),
@@ -73,6 +90,10 @@ export class Game {
73
  if (KEYS.JUMP.includes(e.code)) {
74
  this.keys.jump = true;
75
  e.preventDefault();
 
 
 
 
76
  }
77
  };
78
 
@@ -125,6 +146,7 @@ export class Game {
125
  hitBlock.hasCoin = false;
126
  this.state.player.coins++;
127
  this.state.player.score += 100;
 
128
  }
129
 
130
  // Check coin collection
@@ -135,6 +157,7 @@ export class Game {
135
  this.state.blocks.splice(index, 1);
136
  this.state.player.coins++;
137
  this.state.player.score += 50;
 
138
  }
139
  });
140
 
@@ -154,6 +177,7 @@ export class Game {
154
  enemy.isAlive = false;
155
  this.state.player.vy = -8; // Bounce off enemy
156
  this.state.player.score += 200;
 
157
  } else if (playerHit) {
158
  this.gameOver(false);
159
  }
@@ -162,7 +186,7 @@ export class Game {
162
 
163
  // Check flag collision (win)
164
  if (this.collision.checkFlagCollision(this.state.player, this.state.level.flagPosition)) {
165
- this.gameOver(true);
166
  }
167
 
168
  // Check fall death
@@ -175,7 +199,7 @@ export class Game {
175
  }
176
 
177
  private render(): void {
178
- this.renderer.render(this.state);
179
  }
180
 
181
  private gameLoop = (): void => {
@@ -187,26 +211,66 @@ export class Game {
187
  }
188
  };
189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  private gameOver(isWin: boolean): void {
191
  this.state.isGameOver = true;
192
  this.state.isWin = isWin;
193
- this.state.player.isAlive = !isWin ? false : true;
194
 
195
- if (isWin) {
196
- this.state.player.score += 1000; // Bonus for completing level
197
- }
198
 
199
  // Render final frame
200
  this.render();
201
 
202
- if (this.onGameOver) {
203
  setTimeout(() => {
204
- this.onGameOver!(isWin, this.state.player.score);
205
- }, 1000);
206
  }
207
  }
208
 
209
  start(): void {
 
210
  this.state = this.createInitialState();
211
  this.state.isRunning = true;
212
  this.gameLoop();
@@ -218,19 +282,36 @@ export class Game {
218
  cancelAnimationFrame(this.animationId);
219
  this.animationId = null;
220
  }
 
221
  }
222
 
223
  reset(): void {
224
  this.stop();
 
 
 
225
  this.state = this.createInitialState();
226
  this.render();
227
  }
228
 
 
 
 
 
 
229
  setOnGameOver(callback: (isWin: boolean, score: number) => void): void {
230
- this.onGameOver = callback;
231
  }
232
 
233
  getState(): GameState {
234
  return this.state;
235
  }
 
 
 
 
 
 
 
 
236
  }
 
4
  import { Renderer } from './Renderer';
5
  import { Physics } from './Physics';
6
  import { Collision } from './Collision';
7
+ import { LevelManager, LevelTheme } from './LevelManager';
8
+ import { getAudioManager, SoundEffect } from './AudioManager';
9
  import { GAME_CONFIG, KEYS } from '../constants';
10
 
11
  const { CANVAS_WIDTH, CANVAS_HEIGHT, LEVEL_HEIGHT, PLAYER_WIDTH, PLAYER_HEIGHT } = GAME_CONFIG;
12
 
13
+ export interface GameCallbacks {
14
+ onGameOver?: (isWin: boolean, score: number) => void;
15
+ onLevelComplete?: (levelNumber: number, nextLevel: number | null) => void;
16
+ onGameComplete?: (totalScore: number) => void;
17
+ }
18
+
19
  export class Game {
20
  private canvas: HTMLCanvasElement;
21
  private ctx: CanvasRenderingContext2D;
22
  private renderer: Renderer;
23
  private physics: Physics;
24
  private collision: Collision;
25
+ private levelManager: LevelManager;
26
  private state: GameState;
27
  private keys: KeyState;
28
  private animationId: number | null = null;
29
+ private callbacks: GameCallbacks = {};
30
+ private totalScore: number = 0;
31
+ private currentTheme: LevelTheme = 'overworld';
32
 
33
+ constructor(canvas: HTMLCanvasElement, startLevel: number = 0) {
34
  this.canvas = canvas;
35
  this.ctx = canvas.getContext('2d')!;
36
  this.renderer = new Renderer(this.ctx);
37
  this.physics = new Physics();
38
  this.collision = new Collision();
39
+ this.levelManager = new LevelManager();
40
  this.keys = { left: false, right: false, jump: false };
 
41
 
42
+ // Set starting level
43
+ if (startLevel > 0) {
44
+ this.levelManager.setLevel(startLevel);
45
+ }
46
+
47
+ this.state = this.createInitialState();
48
+ this.currentTheme = this.levelManager.getCurrentLevelInfo().theme;
49
  this.setupEventListeners();
50
  }
51
 
52
  private createInitialState(): GameState {
53
+ const level = this.levelManager.getCurrentLevel();
54
 
55
  return {
56
  player: {
 
64
  isFalling: true,
65
  facingRight: true,
66
  isAlive: true,
67
+ score: this.totalScore,
68
  coins: 0,
69
  },
70
  enemies: level.enemies.map(e => ({ ...e })),
 
90
  if (KEYS.JUMP.includes(e.code)) {
91
  this.keys.jump = true;
92
  e.preventDefault();
93
+ // Play jump sound
94
+ if (this.state.isRunning && !this.state.player.isJumping && !this.state.player.isFalling) {
95
+ getAudioManager().play('jump');
96
+ }
97
  }
98
  };
99
 
 
146
  hitBlock.hasCoin = false;
147
  this.state.player.coins++;
148
  this.state.player.score += 100;
149
+ getAudioManager().play('coin');
150
  }
151
 
152
  // Check coin collection
 
157
  this.state.blocks.splice(index, 1);
158
  this.state.player.coins++;
159
  this.state.player.score += 50;
160
+ getAudioManager().play('coin');
161
  }
162
  });
163
 
 
177
  enemy.isAlive = false;
178
  this.state.player.vy = -8; // Bounce off enemy
179
  this.state.player.score += 200;
180
+ getAudioManager().play('stomp');
181
  } else if (playerHit) {
182
  this.gameOver(false);
183
  }
 
186
 
187
  // Check flag collision (win)
188
  if (this.collision.checkFlagCollision(this.state.player, this.state.level.flagPosition)) {
189
+ this.levelComplete();
190
  }
191
 
192
  // Check fall death
 
199
  }
200
 
201
  private render(): void {
202
+ this.renderer.render(this.state, this.currentTheme, this.levelManager.getCurrentLevelNumber());
203
  }
204
 
205
  private gameLoop = (): void => {
 
211
  }
212
  };
213
 
214
+ private levelComplete(): void {
215
+ this.state.isGameOver = true;
216
+ this.state.isWin = true;
217
+ this.state.player.score += 1000; // Level completion bonus
218
+ this.totalScore = this.state.player.score;
219
+
220
+ getAudioManager().play('levelup');
221
+
222
+ // Render final frame
223
+ this.render();
224
+
225
+ const currentLevelNum = this.levelManager.getCurrentLevelNumber();
226
+
227
+ // Check if there are more levels
228
+ if (!this.levelManager.isLastLevel()) {
229
+ // Proceed to next level after delay
230
+ setTimeout(() => {
231
+ const nextLevel = this.levelManager.nextLevel();
232
+ if (nextLevel) {
233
+ this.currentTheme = this.levelManager.getCurrentLevelInfo().theme;
234
+ this.state = this.createInitialState();
235
+ this.state.isRunning = true;
236
+ this.gameLoop();
237
+
238
+ if (this.callbacks.onLevelComplete) {
239
+ this.callbacks.onLevelComplete(currentLevelNum, this.levelManager.getCurrentLevelNumber());
240
+ }
241
+ }
242
+ }, 2000);
243
+ } else {
244
+ // Game complete!
245
+ setTimeout(() => {
246
+ if (this.callbacks.onGameComplete) {
247
+ this.callbacks.onGameComplete(this.totalScore);
248
+ } else if (this.callbacks.onGameOver) {
249
+ this.callbacks.onGameOver(true, this.totalScore);
250
+ }
251
+ }, 2000);
252
+ }
253
+ }
254
+
255
  private gameOver(isWin: boolean): void {
256
  this.state.isGameOver = true;
257
  this.state.isWin = isWin;
258
+ this.state.player.isAlive = false;
259
 
260
+ getAudioManager().play('death');
 
 
261
 
262
  // Render final frame
263
  this.render();
264
 
265
+ if (this.callbacks.onGameOver) {
266
  setTimeout(() => {
267
+ this.callbacks.onGameOver!(false, this.state.player.score);
268
+ }, 1500);
269
  }
270
  }
271
 
272
  start(): void {
273
+ getAudioManager().resume();
274
  this.state = this.createInitialState();
275
  this.state.isRunning = true;
276
  this.gameLoop();
 
282
  cancelAnimationFrame(this.animationId);
283
  this.animationId = null;
284
  }
285
+ getAudioManager().stopBGM();
286
  }
287
 
288
  reset(): void {
289
  this.stop();
290
+ this.totalScore = 0;
291
+ this.levelManager.resetToFirstLevel();
292
+ this.currentTheme = this.levelManager.getCurrentLevelInfo().theme;
293
  this.state = this.createInitialState();
294
  this.render();
295
  }
296
 
297
+ setCallbacks(callbacks: GameCallbacks): void {
298
+ this.callbacks = callbacks;
299
+ }
300
+
301
+ // Legacy support
302
  setOnGameOver(callback: (isWin: boolean, score: number) => void): void {
303
+ this.callbacks.onGameOver = callback;
304
  }
305
 
306
  getState(): GameState {
307
  return this.state;
308
  }
309
+
310
+ getCurrentLevel(): number {
311
+ return this.levelManager.getCurrentLevelNumber();
312
+ }
313
+
314
+ getTotalLevels(): number {
315
+ return this.levelManager.getLevelCount();
316
+ }
317
  }
lib/engine/Level2.ts ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Level 2 - Underground Cave Theme
2
+
3
+ import { Level, Block, Enemy } from './types';
4
+ import { GAME_CONFIG } from '../constants';
5
+
6
+ const { TILE_SIZE, LEVEL_HEIGHT } = GAME_CONFIG;
7
+
8
+ const LEVEL_2_WIDTH = 3600;
9
+
10
+ export function createLevel2(): Level {
11
+ const blocks: Block[] = [];
12
+ const enemies: Enemy[] = [];
13
+
14
+ // Ground blocks with more gaps
15
+ for (let x = 0; x < LEVEL_2_WIDTH; x += TILE_SIZE) {
16
+ // Multiple gaps for underground feel
17
+ const gaps = [
18
+ { start: 800, end: 900 },
19
+ { start: 1400, end: 1550 },
20
+ { start: 2200, end: 2350 },
21
+ { start: 2800, end: 2900 },
22
+ ];
23
+
24
+ const inGap = gaps.some(gap => x >= gap.start && x < gap.end);
25
+ if (inGap) continue;
26
+
27
+ // Ground layer (2 blocks high)
28
+ blocks.push({
29
+ x,
30
+ y: LEVEL_HEIGHT - TILE_SIZE,
31
+ width: TILE_SIZE,
32
+ height: TILE_SIZE,
33
+ type: 'ground',
34
+ });
35
+ blocks.push({
36
+ x,
37
+ y: LEVEL_HEIGHT - TILE_SIZE * 2,
38
+ width: TILE_SIZE,
39
+ height: TILE_SIZE,
40
+ type: 'ground',
41
+ });
42
+ }
43
+
44
+ // Ceiling blocks for underground feel
45
+ for (let x = 0; x < LEVEL_2_WIDTH; x += TILE_SIZE) {
46
+ blocks.push({
47
+ x,
48
+ y: 0,
49
+ width: TILE_SIZE,
50
+ height: TILE_SIZE,
51
+ type: 'ground',
52
+ });
53
+ blocks.push({
54
+ x,
55
+ y: TILE_SIZE,
56
+ width: TILE_SIZE,
57
+ height: TILE_SIZE,
58
+ type: 'ground',
59
+ });
60
+ }
61
+
62
+ // Underground platforms - brick style
63
+ const platforms = [
64
+ // First section - stepping stones
65
+ { x: 200, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
66
+ { x: 280, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
67
+ { x: 360, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
68
+
69
+ // Question blocks with coins
70
+ { x: 500, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'question' as const, hasCoin: true },
71
+ { x: 600, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'question' as const, hasCoin: true },
72
+
73
+ // Long platform section
74
+ { x: 700, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
75
+ { x: 732, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
76
+ { x: 764, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
77
+
78
+ // After first gap
79
+ { x: 950, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
80
+ { x: 1000, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'question' as const, hasCoin: true },
81
+ { x: 1050, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
82
+
83
+ // Mid-level platforms
84
+ { x: 1200, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
85
+ { x: 1232, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
86
+ { x: 1264, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
87
+ { x: 1296, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
88
+
89
+ // High platform section
90
+ { x: 1600, y: LEVEL_HEIGHT - TILE_SIZE * 7, type: 'brick' as const },
91
+ { x: 1632, y: LEVEL_HEIGHT - TILE_SIZE * 7, type: 'brick' as const },
92
+ { x: 1664, y: LEVEL_HEIGHT - TILE_SIZE * 7, type: 'question' as const, hasCoin: true },
93
+ { x: 1696, y: LEVEL_HEIGHT - TILE_SIZE * 7, type: 'brick' as const },
94
+
95
+ // Low platforms
96
+ { x: 1800, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
97
+ { x: 1900, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
98
+ { x: 2000, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
99
+
100
+ // After gaps section
101
+ { x: 2400, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
102
+ { x: 2500, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'question' as const, hasCoin: true },
103
+ { x: 2600, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
104
+
105
+ // Final staircase
106
+ { x: 3000, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
107
+ { x: 3032, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
108
+ { x: 3032, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'ground' as const },
109
+ { x: 3064, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
110
+ { x: 3064, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'ground' as const },
111
+ { x: 3064, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'ground' as const },
112
+ { x: 3096, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
113
+ { x: 3096, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'ground' as const },
114
+ { x: 3096, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'ground' as const },
115
+ { x: 3096, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'ground' as const },
116
+ ];
117
+
118
+ platforms.forEach(p => {
119
+ blocks.push({
120
+ x: p.x,
121
+ y: p.y,
122
+ width: TILE_SIZE,
123
+ height: TILE_SIZE,
124
+ type: p.type,
125
+ hasCoin: p.hasCoin,
126
+ });
127
+ });
128
+
129
+ // More pipes in underground
130
+ const pipes = [
131
+ { x: 400, height: 2 },
132
+ { x: 1100, height: 3 },
133
+ { x: 1750, height: 2 },
134
+ { x: 2100, height: 4 },
135
+ { x: 2700, height: 2 },
136
+ ];
137
+
138
+ pipes.forEach(pipe => {
139
+ for (let h = 0; h < pipe.height; h++) {
140
+ blocks.push({
141
+ x: pipe.x,
142
+ y: LEVEL_HEIGHT - TILE_SIZE * 2 - (h * TILE_SIZE),
143
+ width: TILE_SIZE * 2,
144
+ height: TILE_SIZE,
145
+ type: 'pipe',
146
+ });
147
+ }
148
+ });
149
+
150
+ // Floating coins
151
+ const coinPositions = [
152
+ { x: 250, y: LEVEL_HEIGHT - TILE_SIZE * 7 },
153
+ { x: 282, y: LEVEL_HEIGHT - TILE_SIZE * 8 },
154
+ { x: 314, y: LEVEL_HEIGHT - TILE_SIZE * 7 },
155
+ { x: 1650, y: LEVEL_HEIGHT - TILE_SIZE * 9 },
156
+ { x: 2550, y: LEVEL_HEIGHT - TILE_SIZE * 8 },
157
+ ];
158
+
159
+ coinPositions.forEach(pos => {
160
+ blocks.push({
161
+ x: pos.x,
162
+ y: pos.y,
163
+ width: 24,
164
+ height: 24,
165
+ type: 'coin',
166
+ });
167
+ });
168
+
169
+ // More enemies - including Koopas
170
+ const enemyPositions = [
171
+ { x: 350, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
172
+ { x: 650, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
173
+ { x: 750, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'goomba' as const },
174
+ { x: 1050, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'koopa' as const },
175
+ { x: 1350, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
176
+ { x: 1650, y: LEVEL_HEIGHT - TILE_SIZE * 9, type: 'goomba' as const },
177
+ { x: 1950, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'koopa' as const },
178
+ { x: 2450, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
179
+ { x: 2650, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
180
+ ];
181
+
182
+ enemyPositions.forEach(pos => {
183
+ enemies.push({
184
+ x: pos.x,
185
+ y: pos.y,
186
+ vx: -1,
187
+ vy: 0,
188
+ width: 32,
189
+ height: pos.type === 'koopa' ? 40 : 32,
190
+ type: pos.type,
191
+ isAlive: true,
192
+ direction: -1,
193
+ });
194
+ });
195
+
196
+ return {
197
+ width: LEVEL_2_WIDTH,
198
+ height: LEVEL_HEIGHT,
199
+ blocks,
200
+ enemies,
201
+ startPosition: { x: 64, y: LEVEL_HEIGHT - TILE_SIZE * 4 },
202
+ flagPosition: { x: 3200, y: LEVEL_HEIGHT - TILE_SIZE * 10 },
203
+ };
204
+ }
lib/engine/Level3.ts ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Level 3 - Castle Theme
2
+
3
+ import { Level, Block, Enemy } from './types';
4
+ import { GAME_CONFIG } from '../constants';
5
+
6
+ const { TILE_SIZE, LEVEL_HEIGHT } = GAME_CONFIG;
7
+
8
+ const LEVEL_3_WIDTH = 4000;
9
+
10
+ export function createLevel3(): Level {
11
+ const blocks: Block[] = [];
12
+ const enemies: Enemy[] = [];
13
+
14
+ // Ground blocks with lava pits
15
+ for (let x = 0; x < LEVEL_3_WIDTH; x += TILE_SIZE) {
16
+ // Lava pit positions (deadly gaps)
17
+ const lavaPits = [
18
+ { start: 600, end: 750 },
19
+ { start: 1200, end: 1400 },
20
+ { start: 1800, end: 1950 },
21
+ { start: 2400, end: 2600 },
22
+ { start: 3000, end: 3150 },
23
+ ];
24
+
25
+ const inLava = lavaPits.some(pit => x >= pit.start && x < pit.end);
26
+ if (inLava) continue;
27
+
28
+ // Castle floor (darker ground)
29
+ blocks.push({
30
+ x,
31
+ y: LEVEL_HEIGHT - TILE_SIZE,
32
+ width: TILE_SIZE,
33
+ height: TILE_SIZE,
34
+ type: 'ground',
35
+ });
36
+ blocks.push({
37
+ x,
38
+ y: LEVEL_HEIGHT - TILE_SIZE * 2,
39
+ width: TILE_SIZE,
40
+ height: TILE_SIZE,
41
+ type: 'ground',
42
+ });
43
+ }
44
+
45
+ // Castle walls and platforms
46
+ const platforms = [
47
+ // Early section - stepping platforms
48
+ { x: 200, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
49
+ { x: 300, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
50
+ { x: 400, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'question' as const, hasCoin: true },
51
+ { x: 500, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
52
+
53
+ // Over first lava pit
54
+ { x: 620, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
55
+ { x: 680, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
56
+ { x: 740, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
57
+
58
+ // Mid section
59
+ { x: 850, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
60
+ { x: 950, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'question' as const, hasCoin: true },
61
+ { x: 1050, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
62
+ { x: 1100, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
63
+
64
+ // Over second lava pit - tricky jumps
65
+ { x: 1220, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
66
+ { x: 1280, y: LEVEL_HEIGHT - TILE_SIZE * 7, type: 'brick' as const },
67
+ { x: 1340, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
68
+
69
+ // Question block cluster
70
+ { x: 1500, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'question' as const, hasCoin: true },
71
+ { x: 1532, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'question' as const, hasCoin: true },
72
+ { x: 1564, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'question' as const, hasCoin: true },
73
+
74
+ // High platform section
75
+ { x: 1650, y: LEVEL_HEIGHT - TILE_SIZE * 8, type: 'brick' as const },
76
+ { x: 1682, y: LEVEL_HEIGHT - TILE_SIZE * 8, type: 'brick' as const },
77
+ { x: 1714, y: LEVEL_HEIGHT - TILE_SIZE * 8, type: 'brick' as const },
78
+
79
+ // Over third lava pit
80
+ { x: 1820, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
81
+ { x: 1880, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
82
+ { x: 1940, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
83
+
84
+ // Long platform run
85
+ { x: 2050, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
86
+ { x: 2082, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
87
+ { x: 2114, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
88
+ { x: 2146, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
89
+ { x: 2178, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
90
+ { x: 2210, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
91
+ { x: 2242, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
92
+ { x: 2274, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
93
+
94
+ // Over fourth lava pit
95
+ { x: 2420, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
96
+ { x: 2480, y: LEVEL_HEIGHT - TILE_SIZE * 7, type: 'question' as const, hasCoin: true },
97
+ { x: 2540, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
98
+
99
+ // Pre-boss section
100
+ { x: 2700, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'brick' as const },
101
+ { x: 2800, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
102
+ { x: 2900, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
103
+
104
+ // Over fifth lava pit
105
+ { x: 3020, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
106
+ { x: 3080, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'brick' as const },
107
+ { x: 3140, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'brick' as const },
108
+
109
+ // Final staircase to flag
110
+ { x: 3300, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
111
+ { x: 3332, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
112
+ { x: 3332, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'ground' as const },
113
+ { x: 3364, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
114
+ { x: 3364, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'ground' as const },
115
+ { x: 3364, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'ground' as const },
116
+ { x: 3396, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
117
+ { x: 3396, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'ground' as const },
118
+ { x: 3396, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'ground' as const },
119
+ { x: 3396, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'ground' as const },
120
+ { x: 3428, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'ground' as const },
121
+ { x: 3428, y: LEVEL_HEIGHT - TILE_SIZE * 4, type: 'ground' as const },
122
+ { x: 3428, y: LEVEL_HEIGHT - TILE_SIZE * 5, type: 'ground' as const },
123
+ { x: 3428, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'ground' as const },
124
+ { x: 3428, y: LEVEL_HEIGHT - TILE_SIZE * 7, type: 'ground' as const },
125
+ ];
126
+
127
+ platforms.forEach(p => {
128
+ blocks.push({
129
+ x: p.x,
130
+ y: p.y,
131
+ width: TILE_SIZE,
132
+ height: TILE_SIZE,
133
+ type: p.type,
134
+ hasCoin: p.hasCoin,
135
+ });
136
+ });
137
+
138
+ // Fewer pipes in castle
139
+ const pipes = [
140
+ { x: 800, height: 2 },
141
+ { x: 2000, height: 2 },
142
+ { x: 2650, height: 3 },
143
+ ];
144
+
145
+ pipes.forEach(pipe => {
146
+ for (let h = 0; h < pipe.height; h++) {
147
+ blocks.push({
148
+ x: pipe.x,
149
+ y: LEVEL_HEIGHT - TILE_SIZE * 2 - (h * TILE_SIZE),
150
+ width: TILE_SIZE * 2,
151
+ height: TILE_SIZE,
152
+ type: 'pipe',
153
+ });
154
+ }
155
+ });
156
+
157
+ // Floating coins - harder to get
158
+ const coinPositions = [
159
+ { x: 680, y: LEVEL_HEIGHT - TILE_SIZE * 8 },
160
+ { x: 1280, y: LEVEL_HEIGHT - TILE_SIZE * 9 },
161
+ { x: 1880, y: LEVEL_HEIGHT - TILE_SIZE * 8 },
162
+ { x: 2480, y: LEVEL_HEIGHT - TILE_SIZE * 9 },
163
+ { x: 3080, y: LEVEL_HEIGHT - TILE_SIZE * 8 },
164
+ ];
165
+
166
+ coinPositions.forEach(pos => {
167
+ blocks.push({
168
+ x: pos.x,
169
+ y: pos.y,
170
+ width: 24,
171
+ height: 24,
172
+ type: 'coin',
173
+ });
174
+ });
175
+
176
+ // Many enemies - challenging!
177
+ const enemyPositions = [
178
+ { x: 250, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
179
+ { x: 450, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'koopa' as const },
180
+ { x: 900, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
181
+ { x: 1000, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
182
+ { x: 1450, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'koopa' as const },
183
+ { x: 1700, y: LEVEL_HEIGHT - TILE_SIZE * 10, type: 'goomba' as const },
184
+ { x: 2100, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'goomba' as const },
185
+ { x: 2200, y: LEVEL_HEIGHT - TILE_SIZE * 6, type: 'koopa' as const },
186
+ { x: 2750, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
187
+ { x: 2850, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'goomba' as const },
188
+ { x: 3200, y: LEVEL_HEIGHT - TILE_SIZE * 3, type: 'koopa' as const },
189
+ ];
190
+
191
+ enemyPositions.forEach(pos => {
192
+ enemies.push({
193
+ x: pos.x,
194
+ y: pos.y,
195
+ vx: -1,
196
+ vy: 0,
197
+ width: 32,
198
+ height: pos.type === 'koopa' ? 40 : 32,
199
+ type: pos.type,
200
+ isAlive: true,
201
+ direction: -1,
202
+ });
203
+ });
204
+
205
+ return {
206
+ width: LEVEL_3_WIDTH,
207
+ height: LEVEL_HEIGHT,
208
+ blocks,
209
+ enemies,
210
+ startPosition: { x: 64, y: LEVEL_HEIGHT - TILE_SIZE * 4 },
211
+ flagPosition: { x: 3550, y: LEVEL_HEIGHT - TILE_SIZE * 10 },
212
+ };
213
+ }
lib/engine/LevelManager.ts ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Level Manager - handles multiple levels and level transitions
2
+
3
+ import { Level } from './types';
4
+ import { createLevel1 } from './Level';
5
+ import { createLevel2 } from './Level2';
6
+ import { createLevel3 } from './Level3';
7
+
8
+ export type LevelTheme = 'overworld' | 'underground' | 'castle';
9
+
10
+ export interface LevelInfo {
11
+ id: number;
12
+ name: string;
13
+ theme: LevelTheme;
14
+ createLevel: () => Level;
15
+ }
16
+
17
+ export class LevelManager {
18
+ private levels: LevelInfo[] = [
19
+ {
20
+ id: 1,
21
+ name: 'World 1-1',
22
+ theme: 'overworld',
23
+ createLevel: createLevel1,
24
+ },
25
+ {
26
+ id: 2,
27
+ name: 'World 1-2',
28
+ theme: 'underground',
29
+ createLevel: createLevel2,
30
+ },
31
+ {
32
+ id: 3,
33
+ name: 'World 1-3',
34
+ theme: 'castle',
35
+ createLevel: createLevel3,
36
+ },
37
+ ];
38
+
39
+ private currentLevelIndex: number = 0;
40
+
41
+ getCurrentLevel(): Level {
42
+ return this.levels[this.currentLevelIndex].createLevel();
43
+ }
44
+
45
+ getCurrentLevelInfo(): LevelInfo {
46
+ return this.levels[this.currentLevelIndex];
47
+ }
48
+
49
+ getLevelCount(): number {
50
+ return this.levels.length;
51
+ }
52
+
53
+ getCurrentLevelNumber(): number {
54
+ return this.currentLevelIndex + 1;
55
+ }
56
+
57
+ setLevel(levelIndex: number): Level | null {
58
+ if (levelIndex >= 0 && levelIndex < this.levels.length) {
59
+ this.currentLevelIndex = levelIndex;
60
+ return this.getCurrentLevel();
61
+ }
62
+ return null;
63
+ }
64
+
65
+ nextLevel(): Level | null {
66
+ if (this.currentLevelIndex < this.levels.length - 1) {
67
+ this.currentLevelIndex++;
68
+ return this.getCurrentLevel();
69
+ }
70
+ return null; // No more levels - game complete!
71
+ }
72
+
73
+ previousLevel(): Level | null {
74
+ if (this.currentLevelIndex > 0) {
75
+ this.currentLevelIndex--;
76
+ return this.getCurrentLevel();
77
+ }
78
+ return null;
79
+ }
80
+
81
+ resetToFirstLevel(): Level {
82
+ this.currentLevelIndex = 0;
83
+ return this.getCurrentLevel();
84
+ }
85
+
86
+ isLastLevel(): boolean {
87
+ return this.currentLevelIndex === this.levels.length - 1;
88
+ }
89
+
90
+ getAllLevels(): LevelInfo[] {
91
+ return [...this.levels];
92
+ }
93
+ }
lib/engine/Renderer.ts CHANGED
@@ -2,9 +2,16 @@
2
 
3
  import { GameState, Block, Enemy, PlayerState, Position } from './types';
4
  import { GAME_CONFIG } from '../constants';
 
5
 
6
  const { CANVAS_WIDTH, CANVAS_HEIGHT, TILE_SIZE, COLORS } = GAME_CONFIG;
7
 
 
 
 
 
 
 
8
  export class Renderer {
9
  private ctx: CanvasRenderingContext2D;
10
  private animationFrame: number = 0;
@@ -13,9 +20,11 @@ export class Renderer {
13
  this.ctx = ctx;
14
  }
15
 
 
 
16
  clear(): void {
17
- // Draw sky background
18
- this.ctx.fillStyle = COLORS.SKY;
19
  this.ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
20
  }
21
 
@@ -248,6 +257,11 @@ export class Renderer {
248
 
249
  if (!enemy.isAlive) return;
250
 
 
 
 
 
 
251
  // Goomba body
252
  this.ctx.fillStyle = COLORS.GOOMBA;
253
 
@@ -289,6 +303,44 @@ export class Renderer {
289
  this.ctx.stroke();
290
  }
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  drawFlag(position: Position, cameraX: number): void {
293
  const screenX = position.x - cameraX;
294
 
@@ -321,10 +373,10 @@ export class Renderer {
321
  this.ctx.fillText('★', screenX + 22 + flagWave / 2, position.y + 25);
322
  }
323
 
324
- drawUI(player: PlayerState): void {
325
  // UI Background
326
  this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
327
- this.ctx.fillRect(10, 10, 200, 60);
328
 
329
  // Score
330
  this.ctx.fillStyle = '#fff';
@@ -341,15 +393,25 @@ export class Renderer {
341
 
342
  this.ctx.fillStyle = '#fff';
343
  this.ctx.fillText(`×${player.coins}`, 165, 40);
 
 
 
 
 
344
  }
345
 
346
  update(): void {
347
  this.animationFrame++;
348
  }
349
 
350
- render(state: GameState): void {
 
351
  this.clear();
352
- this.drawClouds(state.camera.x);
 
 
 
 
353
 
354
  // Draw all blocks
355
  state.blocks.forEach(block => this.drawBlock(block, state.camera.x));
@@ -364,7 +426,7 @@ export class Renderer {
364
  this.drawPlayer(state.player, state.camera.x);
365
 
366
  // Draw UI
367
- this.drawUI(state.player);
368
 
369
  this.update();
370
  }
 
2
 
3
  import { GameState, Block, Enemy, PlayerState, Position } from './types';
4
  import { GAME_CONFIG } from '../constants';
5
+ import { LevelTheme } from './LevelManager';
6
 
7
  const { CANVAS_WIDTH, CANVAS_HEIGHT, TILE_SIZE, COLORS } = GAME_CONFIG;
8
 
9
+ const THEME_COLORS = {
10
+ overworld: { sky: COLORS.SKY, ground: COLORS.GROUND },
11
+ underground: { sky: '#1a1a2e', ground: '#4a4a4a' },
12
+ castle: { sky: '#2d1b1b', ground: '#5a3030' },
13
+ };
14
+
15
  export class Renderer {
16
  private ctx: CanvasRenderingContext2D;
17
  private animationFrame: number = 0;
 
20
  this.ctx = ctx;
21
  }
22
 
23
+ private currentTheme: LevelTheme = 'overworld';
24
+
25
  clear(): void {
26
+ // Draw sky background based on theme
27
+ this.ctx.fillStyle = THEME_COLORS[this.currentTheme].sky;
28
  this.ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
29
  }
30
 
 
257
 
258
  if (!enemy.isAlive) return;
259
 
260
+ if (enemy.type === 'koopa') {
261
+ this.drawKoopa(screenX, enemy);
262
+ return;
263
+ }
264
+
265
  // Goomba body
266
  this.ctx.fillStyle = COLORS.GOOMBA;
267
 
 
303
  this.ctx.stroke();
304
  }
305
 
306
+ private drawKoopa(screenX: number, enemy: Enemy): void {
307
+ // Shell (green)
308
+ this.ctx.fillStyle = '#00aa00';
309
+ this.ctx.beginPath();
310
+ this.ctx.ellipse(screenX + enemy.width / 2, enemy.y + enemy.height - 12, 14, 10, 0, 0, Math.PI * 2);
311
+ this.ctx.fill();
312
+
313
+ // Shell pattern
314
+ this.ctx.strokeStyle = '#006600';
315
+ this.ctx.lineWidth = 2;
316
+ this.ctx.beginPath();
317
+ this.ctx.arc(screenX + enemy.width / 2, enemy.y + enemy.height - 12, 8, 0, Math.PI * 2);
318
+ this.ctx.stroke();
319
+
320
+ // Head
321
+ this.ctx.fillStyle = '#ffcc00';
322
+ this.ctx.beginPath();
323
+ this.ctx.arc(screenX + enemy.width / 2, enemy.y + 10, 10, 0, Math.PI * 2);
324
+ this.ctx.fill();
325
+
326
+ // Eyes
327
+ this.ctx.fillStyle = '#fff';
328
+ this.ctx.fillRect(screenX + enemy.width / 2 - 6, enemy.y + 6, 5, 6);
329
+ this.ctx.fillRect(screenX + enemy.width / 2 + 1, enemy.y + 6, 5, 6);
330
+
331
+ // Pupils
332
+ this.ctx.fillStyle = '#000';
333
+ const pupilOffset = enemy.direction > 0 ? 2 : 0;
334
+ this.ctx.fillRect(screenX + enemy.width / 2 - 4 + pupilOffset, enemy.y + 8, 2, 3);
335
+ this.ctx.fillRect(screenX + enemy.width / 2 + 2 + pupilOffset, enemy.y + 8, 2, 3);
336
+
337
+ // Feet
338
+ const footOffset = Math.sin(this.animationFrame * 0.3) * 2;
339
+ this.ctx.fillStyle = '#ffcc00';
340
+ this.ctx.fillRect(screenX + 4, enemy.y + enemy.height - 6, 8, 6 + footOffset);
341
+ this.ctx.fillRect(screenX + enemy.width - 12, enemy.y + enemy.height - 6, 8, 6 - footOffset);
342
+ }
343
+
344
  drawFlag(position: Position, cameraX: number): void {
345
  const screenX = position.x - cameraX;
346
 
 
373
  this.ctx.fillText('★', screenX + 22 + flagWave / 2, position.y + 25);
374
  }
375
 
376
+ drawUI(player: PlayerState, levelNumber: number = 1): void {
377
  // UI Background
378
  this.ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
379
+ this.ctx.fillRect(10, 10, 280, 60);
380
 
381
  // Score
382
  this.ctx.fillStyle = '#fff';
 
393
 
394
  this.ctx.fillStyle = '#fff';
395
  this.ctx.fillText(`×${player.coins}`, 165, 40);
396
+
397
+ // Level indicator
398
+ this.ctx.fillStyle = '#fff';
399
+ this.ctx.fillText(`WORLD`, 220, 32);
400
+ this.ctx.fillText(`1-${levelNumber}`, 220, 52);
401
  }
402
 
403
  update(): void {
404
  this.animationFrame++;
405
  }
406
 
407
+ render(state: GameState, theme: LevelTheme = 'overworld', levelNumber: number = 1): void {
408
+ this.currentTheme = theme;
409
  this.clear();
410
+
411
+ // Only draw clouds in overworld
412
+ if (theme === 'overworld') {
413
+ this.drawClouds(state.camera.x);
414
+ }
415
 
416
  // Draw all blocks
417
  state.blocks.forEach(block => this.drawBlock(block, state.camera.x));
 
426
  this.drawPlayer(state.player, state.camera.x);
427
 
428
  // Draw UI
429
+ this.drawUI(state.player, levelNumber);
430
 
431
  this.update();
432
  }