@nice2dev/game-engine 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @nice2dev/game-engine — Changelog
2
2
 
3
+ ## 1.1.7
4
+
5
+ ### Patch Changes
6
+
7
+ - Close the three items left open in the 1.1.6 response to OmniVerk.
8
+
9
+ **Every value control works without state in the parent.** The `audit-controllable-state` gate
10
+ counted 87 value props that were required with no `default*`, so the control could not render from a
11
+ manifest without data. It now reports 0 and gates CI at `--max=0`. 48 controls gained `default*` props
12
+ through `useControllableState` (existing `value` + handler call sites behave as before); 33 read-only
13
+ data lists are optional with a stable empty default; 4 props paired with a handler only by name are
14
+ optional; `NiceWhiteboard` and `NiceMetricsDashboard` render an empty state until a session or
15
+ dashboard arrives. Additive API only — nothing renamed or removed.
16
+
17
+ - `NicePivotGrid.fields`, `NiceScheduler.events` and `NiceGitHooks.hooks` are controlled only together
18
+ with their change handler, as `NiceScheduler` already documented for `currentDate`/`view`. A bare
19
+ prop stays an initial value; prop plus handler now always wins (later prop updates used to be
20
+ ignored). `NiceGitHooks.onHookChange` is now called; `onHooksChange` and
21
+ `NiceLocaleManager.onTranslationsChange` are new.
22
+ - `NiceCodeEditor` / `NiceCodeDiffEditor` no longer report a value written from the prop back through
23
+ `onChange`, and call the latest handler rather than the one captured at mount.
24
+ - `NiceHtmlEditor` no longer rewrites its content on every render (React 19 compares the
25
+ `dangerouslySetInnerHTML` object, not its string), which moved the caret; the HTML source and
26
+ Markdown views can be closed again.
27
+ - Selections seeded once from the first item (access manager, automation/scene editors, git diff
28
+ viewer, ribbon, setup wizard) follow data that arrives after mount.
29
+ - `useControllableState`: new `fallbackValue` seed that never triggers the value/default clash warning;
30
+ two functional updates in one tick no longer lose the first; `onChange` is called with one argument
31
+ when there is no meta. `UseControllableStateOptions` is now a union type alias.
32
+
33
+ **`--nice-*` is the source of truth for design tokens.** Controls read only `--nice-*` (9,225 reads in
34
+ 339 files). The unprefixed names (`--color-primary`, `--bg-primary`, `--text-muted`, …) stay declared
35
+ as aliases pointing at the Nice tokens, so reading them works, but setting them no longer repaints Nice
36
+ controls. Apps that relied on that can import `@nice2dev/ui/css/legacy-bg-text-aliases.css` after
37
+ `style.css` (removed in 1.2), or set `--nice-color-primary` and friends directly. 13 tokens that were
38
+ read and never declared (`--color-danger`, `--text-on-primary`, `--color-white`, …) are defined; the
39
+ dark blocks define the canonical names, so `.nice-dark` on `<body>` no longer leaves them on light
40
+ values; `--nice-primary-bg` and `--nice-selected-bg` are a primary tint instead of falling through to
41
+ grey. New gate `audit-token-names`.
42
+
43
+ **Node ESM and `require()`.** `@nice2dev/spatial-core` imports from plain Node ESM (51 relative
44
+ specifiers gained extensions, enforced by `nodenext`), and so does `@nice2dev/ui-buildings`, which
45
+ failed through it. `require()` works for `ui-3d`, `ui-graphics` and `game-engine`. The package-exports
46
+ gate now loads every entry in both module systems.
47
+
48
+ **Hygiene.** Sources are prettier-formatted and CI gates on it; 640 stale `.d.ts` files were removed
49
+ from source trees (never published).
50
+
3
51
  ## 1.1.6
4
52
 
5
53
  No changes in this release.
@@ -37,13 +37,27 @@ class GameConfig {
37
37
  get settings() {
38
38
  return this._settings;
39
39
  }
40
- get width() { return this._settings.width; }
41
- get height() { return this._settings.height; }
42
- get targetFps() { return this._settings.targetFps; }
43
- get maxPlayers() { return this._settings.maxPlayers; }
44
- get physics() { return this._settings.physics; }
45
- get audio() { return this._settings.audio; }
46
- get rendering() { return this._settings.rendering; }
40
+ get width() {
41
+ return this._settings.width;
42
+ }
43
+ get height() {
44
+ return this._settings.height;
45
+ }
46
+ get targetFps() {
47
+ return this._settings.targetFps;
48
+ }
49
+ get maxPlayers() {
50
+ return this._settings.maxPlayers;
51
+ }
52
+ get physics() {
53
+ return this._settings.physics;
54
+ }
55
+ get audio() {
56
+ return this._settings.audio;
57
+ }
58
+ get rendering() {
59
+ return this._settings.rendering;
60
+ }
47
61
  update(overrides) {
48
62
  this._settings = mergeSettings(this._settings, overrides);
49
63
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -24,9 +24,24 @@ function generateServiceWorkerConfig(config) {
24
24
  strategy: config.cacheStrategy,
25
25
  precache: [config.startUrl, '/index.html', '/manifest.json'],
26
26
  runtimeCaching: [
27
- { urlPattern: /\.(?:png|jpg|jpeg|webp|svg|gif)$/i, handler: 'CacheFirst', options: { cacheName: 'images', expiration: { maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 } } },
28
- { urlPattern: /\.(?:js|css)$/i, handler: 'StaleWhileRevalidate', options: { cacheName: 'static' } },
29
- { urlPattern: /\.(?:mp3|ogg|wav)$/i, handler: 'CacheFirst', options: { cacheName: 'audio', expiration: { maxEntries: 50 } } },
27
+ {
28
+ urlPattern: /\.(?:png|jpg|jpeg|webp|svg|gif)$/i,
29
+ handler: 'CacheFirst',
30
+ options: {
31
+ cacheName: 'images',
32
+ expiration: { maxEntries: 100, maxAgeSeconds: 30 * 24 * 60 * 60 },
33
+ },
34
+ },
35
+ {
36
+ urlPattern: /\.(?:js|css)$/i,
37
+ handler: 'StaleWhileRevalidate',
38
+ options: { cacheName: 'static' },
39
+ },
40
+ {
41
+ urlPattern: /\.(?:mp3|ogg|wav)$/i,
42
+ handler: 'CacheFirst',
43
+ options: { cacheName: 'audio', expiration: { maxEntries: 50 } },
44
+ },
30
45
  ],
31
46
  offlineFallback: config.offlineSupport ? '/offline.html' : undefined,
32
47
  };
@@ -124,19 +139,37 @@ function createDefaultMobileLayout() {
124
139
  }
125
140
  const MOBILE_PERF_PROFILES = {
126
141
  low: {
127
- tier: 'low', maxFPS: 30, renderScale: 0.5, maxParticles: 100,
128
- maxAudioSources: 8, shadowQuality: 'off', postProcessing: false,
129
- textureQuality: 'low', physicsBroadPhaseSize: 64,
142
+ tier: 'low',
143
+ maxFPS: 30,
144
+ renderScale: 0.5,
145
+ maxParticles: 100,
146
+ maxAudioSources: 8,
147
+ shadowQuality: 'off',
148
+ postProcessing: false,
149
+ textureQuality: 'low',
150
+ physicsBroadPhaseSize: 64,
130
151
  },
131
152
  medium: {
132
- tier: 'medium', maxFPS: 60, renderScale: 0.75, maxParticles: 500,
133
- maxAudioSources: 16, shadowQuality: 'low', postProcessing: true,
134
- textureQuality: 'medium', physicsBroadPhaseSize: 128,
153
+ tier: 'medium',
154
+ maxFPS: 60,
155
+ renderScale: 0.75,
156
+ maxParticles: 500,
157
+ maxAudioSources: 16,
158
+ shadowQuality: 'low',
159
+ postProcessing: true,
160
+ textureQuality: 'medium',
161
+ physicsBroadPhaseSize: 128,
135
162
  },
136
163
  high: {
137
- tier: 'high', maxFPS: 60, renderScale: 1, maxParticles: 2000,
138
- maxAudioSources: 32, shadowQuality: 'high', postProcessing: true,
139
- textureQuality: 'high', physicsBroadPhaseSize: 256,
164
+ tier: 'high',
165
+ maxFPS: 60,
166
+ renderScale: 1,
167
+ maxParticles: 2000,
168
+ maxAudioSources: 32,
169
+ shadowQuality: 'high',
170
+ postProcessing: true,
171
+ textureQuality: 'high',
172
+ physicsBroadPhaseSize: 256,
140
173
  },
141
174
  };
142
175
 
@@ -15,12 +15,12 @@ const TUTORIAL_ANIMATIONS = {
15
15
  id: 'animations-sprites-tweens',
16
16
  title: {
17
17
  en: 'Animations: Sprites, Tweens & Easing',
18
- pl: 'Animacje: Sprite\'y, Tweeny i Easing',
18
+ pl: "Animacje: Sprite'y, Tweeny i Easing",
19
19
  de: 'Animationen: Sprites, Tweens & Easing',
20
20
  },
21
21
  description: {
22
22
  en: 'Learn to animate game objects using sprite sheets, tweens, easing functions, and state machines.',
23
- pl: 'Naucz się animować obiekty za pomocą sprite sheet\'ów, tweenów, easing\'ów i maszyn stanów.',
23
+ pl: "Naucz się animować obiekty za pomocą sprite sheet'ów, tweenów, easing'ów i maszyn stanów.",
24
24
  de: 'Lerne, Spielobjekte mit Spritesheets, Tweens, Easing und Zustandsmaschinen zu animieren.',
25
25
  },
26
26
  category: 'game-basics',
@@ -41,8 +41,8 @@ const TUTORIAL_ANIMATIONS = {
41
41
  de: 'Willkommen bei Animationen! 🎬',
42
42
  },
43
43
  content: {
44
- en: 'In this tutorial you will learn:\n• Creating animation clips from sprite sheets\n• Using tweens for smooth movement\n• Easing functions (bounce, elastic, cubic)\n• Animation state machines\n\nLet\'s bring your game to life!',
45
- pl: 'W tym tutorialu nauczysz się:\n• Tworzenia klipów animacji ze sprite sheet\'ów\n• Używania tweenów do płynnego ruchu\n• Funkcji easing (bounce, elastic, cubic)\n• Maszyn stanów animacji\n\nOżywmy Twoją grę!',
44
+ en: "In this tutorial you will learn:\n• Creating animation clips from sprite sheets\n• Using tweens for smooth movement\n• Easing functions (bounce, elastic, cubic)\n• Animation state machines\n\nLet's bring your game to life!",
45
+ pl: "W tym tutorialu nauczysz się:\n• Tworzenia klipów animacji ze sprite sheet'ów\n• Używania tweenów do płynnego ruchu\n• Funkcji easing (bounce, elastic, cubic)\n• Maszyn stanów animacji\n\nOżywmy Twoją grę!",
46
46
  de: 'In diesem Tutorial lernst du:\n• Animationsclips aus Spritesheets erstellen\n• Tweens für flüssige Bewegungen\n• Easing-Funktionen (Bounce, Elastic, Cubic)\n• Animations-Zustandsmaschinen\n\nLass uns dein Spiel zum Leben erwecken!',
47
47
  },
48
48
  skippable: true,
@@ -59,7 +59,7 @@ const TUTORIAL_ANIMATIONS = {
59
59
  },
60
60
  content: {
61
61
  en: 'Use `createClipFromSheet` to define an animation clip from a sprite sheet.\nParameters: name, columns in the sheet, start index, frame count, frame size, and duration per frame.',
62
- pl: 'Użyj `createClipFromSheet` aby zdefiniować klip animacji ze sprite sheet\'a.\nParametry: nazwa, kolumny w arkuszu, indeks startowy, ilość klatek, rozmiar klatki i czas trwania klatki.',
62
+ pl: "Użyj `createClipFromSheet` aby zdefiniować klip animacji ze sprite sheet'a.\nParametry: nazwa, kolumny w arkuszu, indeks startowy, ilość klatek, rozmiar klatki i czas trwania klatki.",
63
63
  de: 'Verwende `createClipFromSheet` um einen Animationsclip aus einem Spritesheet zu erstellen.\nParameter: Name, Spalten im Sheet, Startindex, Framezahl, Framegröße und Dauer pro Frame.',
64
64
  },
65
65
  codeTemplate: '// Create a "walk" animation clip\nconst walkClip = ',
@@ -195,7 +195,7 @@ const TUTORIAL_ANIMATIONS = {
195
195
  de: 'Animations-Checkpoint ✅',
196
196
  },
197
197
  content: {
198
- en: 'Great progress! You\'ve learned the core animation APIs. Your progress is saved.',
198
+ en: "Great progress! You've learned the core animation APIs. Your progress is saved.",
199
199
  pl: 'Świetne postępy! Opanowałeś podstawowe API animacji. Twoje postępy zostały zapisane.',
200
200
  de: 'Toller Fortschritt! Du hast die Kern-Animations-APIs gelernt. Dein Fortschritt wurde gespeichert.',
201
201
  },
@@ -251,7 +251,7 @@ const TUTORIAL_TILEMAPS = {
251
251
  de: 'Welten mit Kacheln bauen! 🗺️',
252
252
  },
253
253
  content: {
254
- en: 'Tilemaps are the foundation of 2D game worlds — platformers, RPGs, strategy games.\n\nYou will learn:\n• Creating a tilemap with layers\n• Placing and filling tiles\n• Auto-tiling (smart borders)\n• Collision detection with tiles\n• Serializing tilemaps (save/load)\n\nLet\'s build a world! 🌍',
254
+ en: "Tilemaps are the foundation of 2D game worlds — platformers, RPGs, strategy games.\n\nYou will learn:\n• Creating a tilemap with layers\n• Placing and filling tiles\n• Auto-tiling (smart borders)\n• Collision detection with tiles\n• Serializing tilemaps (save/load)\n\nLet's build a world! 🌍",
255
255
  pl: 'Tilemapy to fundament świata gier 2D — platformówek, RPG, strategii.\n\nNauczysz się:\n• Tworzenia tilemapy z warstwami\n• Umieszczania i wypełniania kafelków\n• Auto-tilingu (inteligentne krawędzie)\n• Detekcji kolizji z kafelkami\n• Serializacji tilemap (zapis/odczyt)\n\nZbudujmy świat! 🌍',
256
256
  de: 'Tilemaps sind das Fundament von 2D-Spielwelten — Platformer, RPGs, Strategiespiele.\n\nDu lernst:\n• Erstellen einer Tilemap mit Ebenen\n• Kacheln platzieren und füllen\n• Auto-Tiling (intelligente Ränder)\n• Kollisionserkennung mit Kacheln\n• Tilemaps serialisieren (Speichern/Laden)\n\nBauen wir eine Welt! 🌍',
257
257
  },
@@ -497,7 +497,7 @@ const TUTORIAL_VISUAL_SCRIPTING = {
497
497
  de: 'Baue einen Script-Graphen',
498
498
  },
499
499
  content: {
500
- en: 'A `ScriptGraph` has nodes (instances) and connections between their ports.\nLet\'s create a simple graph that moves an entity when a key is pressed.',
500
+ en: "A `ScriptGraph` has nodes (instances) and connections between their ports.\nLet's create a simple graph that moves an entity when a key is pressed.",
501
501
  pl: '`ScriptGraph` zawiera węzły (instancje) i połączenia między ich portami.\nUtwórzmy prosty graf, który przesuwa encję po naciśnięciu klawisza.',
502
502
  de: 'Ein `ScriptGraph` hat Knoten (Instanzen) und Verbindungen zwischen Ports.\nErstellen wir einen einfachen Graphen, der eine Entity bewegt, wenn eine Taste gedrückt wird.',
503
503
  },
@@ -527,9 +527,26 @@ const TUTORIAL_VISUAL_SCRIPTING = {
527
527
  de: 'Was steuert ein "flow"-Port in Visual Scripting?',
528
528
  },
529
529
  quizOptions: [
530
- { id: 'a', text: { en: 'The color of the node', pl: 'Kolor węzła', de: 'Die Farbe des Knotens' } },
531
- { id: 'b', text: { en: 'The execution order (which node runs next)', pl: 'Kolejność wykonania (który węzeł uruchomić dalej)', de: 'Die Ausführungsreihenfolge (welcher Knoten als nächstes läuft)' } },
532
- { id: 'c', text: { en: 'The animation speed', pl: 'Prędkość animacji', de: 'Die Animationsgeschwindigkeit' } },
530
+ {
531
+ id: 'a',
532
+ text: { en: 'The color of the node', pl: 'Kolor węzła', de: 'Die Farbe des Knotens' },
533
+ },
534
+ {
535
+ id: 'b',
536
+ text: {
537
+ en: 'The execution order (which node runs next)',
538
+ pl: 'Kolejność wykonania (który węzeł uruchomić dalej)',
539
+ de: 'Die Ausführungsreihenfolge (welcher Knoten als nächstes läuft)',
540
+ },
541
+ },
542
+ {
543
+ id: 'c',
544
+ text: {
545
+ en: 'The animation speed',
546
+ pl: 'Prędkość animacji',
547
+ de: 'Die Animationsgeschwindigkeit',
548
+ },
549
+ },
533
550
  { id: 'd', text: { en: 'The sound volume', pl: 'Głośność dźwięku', de: 'Die Lautstärke' } },
534
551
  ],
535
552
  quizCorrectId: 'b',
@@ -544,7 +561,7 @@ const TUTORIAL_VISUAL_SCRIPTING = {
544
561
  de: 'Starte den Graphen! 🚀',
545
562
  },
546
563
  content: {
547
- en: 'The `GraphExecutor` takes a node library and a script graph, then executes it each frame.\nConnect it to the engine\'s update loop.',
564
+ en: "The `GraphExecutor` takes a node library and a script graph, then executes it each frame.\nConnect it to the engine's update loop.",
548
565
  pl: '`GraphExecutor` przyjmuje bibliotekę węzłów i graf, a następnie wykonuje go co klatkę.\nPodłącz go do pętli update silnika.',
549
566
  de: 'Der `GraphExecutor` nimmt eine Knoten-Bibliothek und einen Graphen und führt ihn pro Frame aus.\nVerbinde ihn mit der Update-Schleife der Engine.',
550
567
  },
@@ -569,7 +586,7 @@ const TUTORIAL_VISUAL_SCRIPTING = {
569
586
  de: 'Probiere es! Verbinde Knoten',
570
587
  },
571
588
  content: {
572
- en: 'In the editor, try dragging a connection from one node\'s output port to another node\'s input port.\nThe wire turns green when compatible ports are connected!',
589
+ en: "In the editor, try dragging a connection from one node's output port to another node's input port.\nThe wire turns green when compatible ports are connected!",
573
590
  pl: 'W edytorze spróbuj przeciągnąć połączenie z portu wyjściowego jednego węzła na port wejściowy drugiego.\nPrzewód zmienia kolor na zielony gdy porty są kompatybilne!',
574
591
  de: 'Im Editor: Ziehe eine Verbindung vom Ausgang eines Knotens zum Eingang eines anderen.\nDie Leitung wird grün, wenn kompatible Ports verbunden sind!',
575
592
  },
@@ -626,7 +643,7 @@ const TUTORIAL_LOCAL_MULTIPLAYER = {
626
643
  de: 'Zusammen spielen! 🎮🎮',
627
644
  },
628
645
  content: {
629
- en: 'Local multiplayer lets friends play together on the same screen!\n\nYou will learn:\n• `PlayerManager` — managing up to 12 players\n• Gamepad input for each player\n• Split-screen viewports\n• Drop-in / drop-out\n\nGrab some controllers and let\'s go! 🕹️',
646
+ en: "Local multiplayer lets friends play together on the same screen!\n\nYou will learn:\n• `PlayerManager` — managing up to 12 players\n• Gamepad input for each player\n• Split-screen viewports\n• Drop-in / drop-out\n\nGrab some controllers and let's go! 🕹️",
630
647
  pl: 'Multiplayer lokalny pozwala znajomym grać razem na jednym ekranie!\n\nNauczysz się:\n• `PlayerManager` — zarządzanie do 12 graczy\n• Wejście z gamepada dla każdego gracza\n• Podzielony ekran (split-screen)\n• Dynamiczne dołączanie/odłączanie graczy\n\nWeź kontrolery i zaczynamy! 🕹️',
631
648
  de: 'Lokaler Multiplayer lässt Freunde zusammen auf einem Bildschirm spielen!\n\nDu lernst:\n• `PlayerManager` — bis zu 12 Spieler verwalten\n• Gamepad-Eingabe für jeden Spieler\n• Splitscreen-Ansichten\n• Drop-in / Drop-out\n\nSchnappt euch Controller und los! 🕹️',
632
649
  },
@@ -769,8 +786,8 @@ const TUTORIAL_LOCAL_MULTIPLAYER = {
769
786
  pl: 'Spraw, aby gamepady zaczęły wibrować przy zdarzeniach jak obrażenia czy eksplozje, używając metody `vibrate`.',
770
787
  de: 'Lass Gamepads bei Events wie Schaden oder Explosionen vibrieren mit der `vibrate`-Methode.',
771
788
  },
772
- codeTemplate: '// Vibrate a player\'s gamepad\n',
773
- codeSolution: '// Vibrate player 1\'s gamepad for 200ms\nengine.input.vibrate(\n 1, // player index\n 200, // duration (ms)\n 0.5, // weak motor magnitude (0-1)\n 1.0 // strong motor magnitude (0-1)\n);',
789
+ codeTemplate: "// Vibrate a player's gamepad\n",
790
+ codeSolution: "// Vibrate player 1's gamepad for 200ms\nengine.input.vibrate(\n 1, // player index\n 200, // duration (ms)\n 0.5, // weak motor magnitude (0-1)\n 1.0 // strong motor magnitude (0-1)\n);",
774
791
  codeValidation: {
775
792
  containsAll: ['vibrate'],
776
793
  },
@@ -162,7 +162,7 @@ const TUTORIAL_MULTIPLAYER_ONLINE = {
162
162
  de: 'Verwende `SnapshotBuffer` zum Speichern von Spielzustand-Snapshots für Interpolation. Der Host sendet Snapshots, Clients interpolieren dazwischen.',
163
163
  },
164
164
  codeTemplate: '// Create snapshot buffer\nconst snapshots = ',
165
- codeSolution: "const snapshots = new SnapshotBuffer(60);\n\n// Host creates and broadcasts snapshots\nconst snapshot = {\n tick: net.tick,\n timestamp: performance.now(),\n states: new Map(),\n};\nsnapshots.push(snapshot);\n\n// Client interpolates\nconst pair = snapshots.getInterpolationPair(serverTime);\nif (pair) {\n const [a, b] = pair;\n const t = (serverTime - a.timestamp) / (b.timestamp - a.timestamp);\n // interpolateState(stateA, stateB, t)\n}",
165
+ codeSolution: 'const snapshots = new SnapshotBuffer(60);\n\n// Host creates and broadcasts snapshots\nconst snapshot = {\n tick: net.tick,\n timestamp: performance.now(),\n states: new Map(),\n};\nsnapshots.push(snapshot);\n\n// Client interpolates\nconst pair = snapshots.getInterpolationPair(serverTime);\nif (pair) {\n const [a, b] = pair;\n const t = (serverTime - a.timestamp) / (b.timestamp - a.timestamp);\n // interpolateState(stateA, stateB, t)\n}',
166
166
  codeValidation: {
167
167
  containsAll: ['SnapshotBuffer', 'push', 'getInterpolationPair'],
168
168
  },
@@ -187,7 +187,7 @@ const TUTORIAL_MULTIPLAYER_ONLINE = {
187
187
  de: 'Verwende `ClientPrediction` zum Speichern lokaler Eingaben und Abgleich nach Server-Bestätigung. Dies eliminiert Input-Lag für den lokalen Spieler.',
188
188
  },
189
189
  codeTemplate: '// Set up prediction\nconst prediction = ',
190
- codeSolution: "const prediction = new ClientPrediction();\n\n// Every frame: record local input + simulated result\nprediction.addInput({\n tick: net.tick,\n inputActions: { moveRight: true, jump: false },\n resultPosition: { x: player.x + 5, y: player.y },\n});\n\n// When server confirms a tick:\nprediction.acknowledge(serverTick);\n\n// Replay unconfirmed inputs after correction\nconst pending = prediction.getPendingInputs();",
190
+ codeSolution: 'const prediction = new ClientPrediction();\n\n// Every frame: record local input + simulated result\nprediction.addInput({\n tick: net.tick,\n inputActions: { moveRight: true, jump: false },\n resultPosition: { x: player.x + 5, y: player.y },\n});\n\n// When server confirms a tick:\nprediction.acknowledge(serverTick);\n\n// Replay unconfirmed inputs after correction\nconst pending = prediction.getPendingInputs();',
191
191
  codeValidation: {
192
192
  containsAll: ['ClientPrediction', 'addInput', 'acknowledge'],
193
193
  },
@@ -212,10 +212,38 @@ const TUTORIAL_MULTIPLAYER_ONLINE = {
212
212
  de: 'Warum ist clientseitige Vorhersage in Online-Spielen wichtig?',
213
213
  },
214
214
  quizOptions: [
215
- { id: 'a', text: { en: 'To reduce server load', pl: 'Aby zmniejszyć obciążenie serwera', de: 'Um die Serverlast zu reduzieren' } },
216
- { id: 'b', text: { en: 'To eliminate perceived input lag', pl: 'Aby wyeliminować odczuwane opóźnienie inputu', de: 'Um wahrgenommene Eingabeverzögerung zu eliminieren' } },
217
- { id: 'c', text: { en: 'To improve graphics', pl: 'Aby poprawić grafikę', de: 'Um die Grafik zu verbessern' } },
218
- { id: 'd', text: { en: 'To save bandwidth', pl: 'Aby oszczędzić transfer', de: 'Um Bandbreite zu sparen' } },
215
+ {
216
+ id: 'a',
217
+ text: {
218
+ en: 'To reduce server load',
219
+ pl: 'Aby zmniejszyć obciążenie serwera',
220
+ de: 'Um die Serverlast zu reduzieren',
221
+ },
222
+ },
223
+ {
224
+ id: 'b',
225
+ text: {
226
+ en: 'To eliminate perceived input lag',
227
+ pl: 'Aby wyeliminować odczuwane opóźnienie inputu',
228
+ de: 'Um wahrgenommene Eingabeverzögerung zu eliminieren',
229
+ },
230
+ },
231
+ {
232
+ id: 'c',
233
+ text: {
234
+ en: 'To improve graphics',
235
+ pl: 'Aby poprawić grafikę',
236
+ de: 'Um die Grafik zu verbessern',
237
+ },
238
+ },
239
+ {
240
+ id: 'd',
241
+ text: {
242
+ en: 'To save bandwidth',
243
+ pl: 'Aby oszczędzić transfer',
244
+ de: 'Um Bandbreite zu sparen',
245
+ },
246
+ },
219
247
  ],
220
248
  quizCorrectId: 'b',
221
249
  },
@@ -254,7 +282,7 @@ const TUTORIAL_MULTIPLAYER_ONLINE = {
254
282
  de: 'Online-Mehrspieler fertig! 🎉',
255
283
  },
256
284
  content: {
257
- en: 'You\'ve learned how to build networked multiplayer games! You can now:\n• Connect peers via WebRTC\n• Create and manage lobby rooms\n• Synchronize game state with snapshots\n• Implement client-side prediction\n• Spawn/destroy entities across the network',
285
+ en: "You've learned how to build networked multiplayer games! You can now:\n• Connect peers via WebRTC\n• Create and manage lobby rooms\n• Synchronize game state with snapshots\n• Implement client-side prediction\n• Spawn/destroy entities across the network",
258
286
  pl: 'Nauczyłeś się budować gry multiplayer online! Teraz potrafisz:\n• Łączyć peerów przez WebRTC\n• Tworzyć i zarządzać pokojami lobby\n• Synchronizować stan gry snapshotami\n• Implementować predykcję po stronie klienta\n• Tworzyć/niszczyć encje w sieci',
259
287
  de: 'Du hast gelernt, vernetzte Multiplayer-Spiele zu bauen! Du kannst jetzt:\n• Peers über WebRTC verbinden\n• Lobby-Räume erstellen und verwalten\n• Spielzustand mit Snapshots synchronisieren\n• Clientseitige Vorhersage implementieren\n• Entities über das Netzwerk spawnen/zerstören',
260
288
  },
@@ -295,7 +323,7 @@ const TUTORIAL_PROCEDURAL_WORLD = {
295
323
  },
296
324
  content: {
297
325
  en: 'Procedural generation creates content using algorithms instead of manual design.\n\nYou will learn:\n• Seeded random numbers (reproducible worlds)\n• Perlin noise (natural-looking terrain)\n• Enemy wave scaling\n• Loot tables with weighted drops\n• Procedural pixel art sprites',
298
- pl: 'Generowanie proceduralne tworzy zawartość za pomocą algorytmów zamiast ręcznego projektowania.\n\nNauczysz się:\n• Losowość z seedem (odtwarzalne światy)\n• Szum Perlina (naturalnie wyglądający teren)\n• Skalowanie fal wrogów\n• Tabele łupów z ważonymi dropami\n• Proceduralne pikselo-artowe sprite\'y',
326
+ pl: "Generowanie proceduralne tworzy zawartość za pomocą algorytmów zamiast ręcznego projektowania.\n\nNauczysz się:\n• Losowość z seedem (odtwarzalne światy)\n• Szum Perlina (naturalnie wyglądający teren)\n• Skalowanie fal wrogów\n• Tabele łupów z ważonymi dropami\n• Proceduralne pikselo-artowe sprite'y",
299
327
  de: 'Prozedurale Generierung erstellt Inhalte mit Algorithmen statt manueller Gestaltung.\n\nDu lernst:\n• Zufallszahlen mit Seed (reproduzierbare Welten)\n• Perlin-Noise (natürlich aussehende Landschaft)\n• Feindwellen-Skalierung\n• Loot-Tabellen mit gewichteten Drops\n• Prozedurale Pixel-Art-Sprites',
300
328
  },
301
329
  skippable: true,
@@ -341,7 +369,7 @@ const TUTORIAL_PROCEDURAL_WORLD = {
341
369
  de: '`PerlinNoise` erzeugt glatte, natürlich aussehende Werte. Verwende `noise2D(x, y)` für Heightmaps und `fbm()` für detaillierte Landschaften.',
342
370
  },
343
371
  codeTemplate: '// Generate a height map\nconst noise = ',
344
- codeSolution: "const noise = new PerlinNoise(42);\n\n// Generate a 64x64 terrain\nfor (let y = 0; y < 64; y++) {\n for (let x = 0; x < 64; x++) {\n // noise2D returns -1 to 1 — scale to 0-1\n const height = (noise.noise2D(x * 0.05, y * 0.05) + 1) * 0.5;\n\n // Use fbm for more detail (4 octaves)\n const detail = noise.fbm(x * 0.1, y * 0.1, 4);\n\n // Map to tile type\n if (height < 0.3) { /* water */ }\n else if (height < 0.6) { /* grass */ }\n else { /* mountain */ }\n }\n}",
372
+ codeSolution: 'const noise = new PerlinNoise(42);\n\n// Generate a 64x64 terrain\nfor (let y = 0; y < 64; y++) {\n for (let x = 0; x < 64; x++) {\n // noise2D returns -1 to 1 — scale to 0-1\n const height = (noise.noise2D(x * 0.05, y * 0.05) + 1) * 0.5;\n\n // Use fbm for more detail (4 octaves)\n const detail = noise.fbm(x * 0.1, y * 0.1, 4);\n\n // Map to tile type\n if (height < 0.3) { /* water */ }\n else if (height < 0.6) { /* grass */ }\n else { /* mountain */ }\n }\n}',
345
373
  codeValidation: {
346
374
  containsAll: ['PerlinNoise', 'noise2D'],
347
375
  },
@@ -407,7 +435,7 @@ const TUTORIAL_PROCEDURAL_WORLD = {
407
435
  type: 'code',
408
436
  title: {
409
437
  en: 'Generate Pixel Art Sprites',
410
- pl: 'Generuj pikselo-artowe sprite\'y',
438
+ pl: "Generuj pikselo-artowe sprite'y",
411
439
  de: 'Pixel-Art-Sprites generieren',
412
440
  },
413
441
  content: {
@@ -416,7 +444,7 @@ const TUTORIAL_PROCEDURAL_WORLD = {
416
444
  de: '`ProceduralSprite` generiert symmetrische Pixel-Art aus einer Palette. Jeder Seed ergibt einen einzigartigen Charakter!',
417
445
  },
418
446
  codeTemplate: '// Generate a procedural sprite\nconst sprite = ',
419
- codeSolution: "const sprite = new ProceduralSprite(8, 8, [\n { r: 0.2, g: 0.5, b: 1, a: 1 }, // blue\n { r: 0.8, g: 0.2, b: 0.3, a: 1 }, // red\n { r: 0.9, g: 0.9, b: 0.1, a: 1 }, // yellow\n]);\n\n// Generate with seed\nconst pixels = sprite.generate(42);\n// Each seed = unique character sprite!",
447
+ codeSolution: 'const sprite = new ProceduralSprite(8, 8, [\n { r: 0.2, g: 0.5, b: 1, a: 1 }, // blue\n { r: 0.8, g: 0.2, b: 0.3, a: 1 }, // red\n { r: 0.9, g: 0.9, b: 0.1, a: 1 }, // yellow\n]);\n\n// Generate with seed\nconst pixels = sprite.generate(42);\n// Each seed = unique character sprite!',
420
448
  codeValidation: {
421
449
  containsAll: ['ProceduralSprite', 'generate'],
422
450
  },
@@ -441,10 +469,34 @@ const TUTORIAL_PROCEDURAL_WORLD = {
441
469
  de: 'Was macht Perlin-Noise besser als rein zufällige Werte für Terrain?',
442
470
  },
443
471
  quizOptions: [
444
- { id: 'a', text: { en: 'It\'s faster to compute', pl: 'Jest szybszy do obliczenia', de: 'Es ist schneller zu berechnen' } },
445
- { id: 'b', text: { en: 'It produces smooth, continuous gradients', pl: 'Produkuje gładkie, ciągłe gradienty', de: 'Es erzeugt glatte, kontinuierliche Gradienten' } },
446
- { id: 'c', text: { en: 'It uses less memory', pl: 'Zużywa mniej pamięci', de: 'Es verbraucht weniger Speicher' } },
447
- { id: 'd', text: { en: 'It\'s more random', pl: 'Jest bardziej losowy', de: 'Es ist zufälliger' } },
472
+ {
473
+ id: 'a',
474
+ text: {
475
+ en: "It's faster to compute",
476
+ pl: 'Jest szybszy do obliczenia',
477
+ de: 'Es ist schneller zu berechnen',
478
+ },
479
+ },
480
+ {
481
+ id: 'b',
482
+ text: {
483
+ en: 'It produces smooth, continuous gradients',
484
+ pl: 'Produkuje gładkie, ciągłe gradienty',
485
+ de: 'Es erzeugt glatte, kontinuierliche Gradienten',
486
+ },
487
+ },
488
+ {
489
+ id: 'c',
490
+ text: {
491
+ en: 'It uses less memory',
492
+ pl: 'Zużywa mniej pamięci',
493
+ de: 'Es verbraucht weniger Speicher',
494
+ },
495
+ },
496
+ {
497
+ id: 'd',
498
+ text: { en: "It's more random", pl: 'Jest bardziej losowy', de: 'Es ist zufälliger' },
499
+ },
448
500
  ],
449
501
  quizCorrectId: 'b',
450
502
  },
@@ -459,7 +511,7 @@ const TUTORIAL_PROCEDURAL_WORLD = {
459
511
  },
460
512
  content: {
461
513
  en: 'You can now generate infinite, unique games! Skills learned:\n• Seeded random for reproducible worlds\n• Perlin noise for natural terrain\n• Wave scaling for enemy difficulty\n• Loot tables for balanced drops\n• Procedural pixel art sprites',
462
- pl: 'Teraz możesz generować nieskończone, unikalne gry! Umiejętności:\n• Losowość z seedem dla odtwarzalnych światów\n• Szum Perlina dla naturalnego terenu\n• Skalowanie fal dla trudności wrogów\n• Tabele łupów dla zbalansowanych dropów\n• Proceduralne pikselo-artowe sprite\'y',
514
+ pl: "Teraz możesz generować nieskończone, unikalne gry! Umiejętności:\n• Losowość z seedem dla odtwarzalnych światów\n• Szum Perlina dla naturalnego terenu\n• Skalowanie fal dla trudności wrogów\n• Tabele łupów dla zbalansowanych dropów\n• Proceduralne pikselo-artowe sprite'y",
463
515
  de: 'Du kannst jetzt unendliche, einzigartige Spiele generieren! Gelernte Fähigkeiten:\n• Seed-Zufall für reproduzierbare Welten\n• Perlin-Noise für natürliches Terrain\n• Wellen-Skalierung für Feind-Schwierigkeit\n• Loot-Tabellen für ausgewogene Drops\n• Prozedurale Pixel-Art-Sprites',
464
516
  },
465
517
  },
@@ -498,7 +550,7 @@ const TUTORIAL_PLATFORMER = {
498
550
  de: 'Baue einen Platformer! 🏃',
499
551
  },
500
552
  content: {
501
- en: 'In this tutorial you\'ll build a complete platformer game!\n\n• Player movement & acceleration\n• Jumping with coyote time\n• Double-jump & wall-slide\n• Dash mechanic\n• Enemy AI (patrol + chase)\n• Coins & collectibles\n• Level with tilemap\n\nLet\'s make Mario-like magic!',
553
+ en: "In this tutorial you'll build a complete platformer game!\n\n• Player movement & acceleration\n• Jumping with coyote time\n• Double-jump & wall-slide\n• Dash mechanic\n• Enemy AI (patrol + chase)\n• Coins & collectibles\n• Level with tilemap\n\nLet's make Mario-like magic!",
502
554
  pl: 'W tym tutorialu zbudujesz kompletną grę platformową!\n\n• Ruch gracza i przyspieszenie\n• Skakanie z coyote time\n• Podwójny skok i wall-slide\n• Mechanika dashu\n• AI wrogów (patrol + pościg)\n• Monety i zbieranie\n• Poziom z tilemapy\n\nZróbmy magię jak Mario!',
503
555
  de: 'In diesem Tutorial baust du ein komplettes Plattformspiel!\n\n• Spielerbewegung & Beschleunigung\n• Springen mit Coyote-Time\n• Doppelsprung & Wall-Slide\n• Dash-Mechanik\n• Feind-KI (Patrouille + Verfolgung)\n• Münzen & Sammelobjekte\n• Level mit Tilemap\n\nLass uns Mario-Magie machen!',
504
556
  },
@@ -519,7 +571,7 @@ const TUTORIAL_PLATFORMER = {
519
571
  pl: 'Skonfiguruj encję gracza z komponentami Transform, Sprite, Collider2D i RigidBody2D.',
520
572
  de: 'Erstelle die Spieler-Entity mit Transform, Sprite, Collider2D und RigidBody2D Komponenten.',
521
573
  },
522
- codeTemplate: "// Create the player\nconst player = world.createEntity();\n",
574
+ codeTemplate: '// Create the player\nconst player = world.createEntity();\n',
523
575
  codeSolution: "const player = world.createEntity();\nworld.addComponent(player, 'Transform', { x: 100, y: 300, scaleX: 1, scaleY: 1, rotation: 0 });\nworld.addComponent(player, 'Sprite', { textureId: 'player', width: 32, height: 32 });\nworld.addComponent(player, 'Collider2D', { type: 'aabb', width: 28, height: 30, offsetX: 2, offsetY: 2, isTrigger: false });\nworld.addComponent(player, 'RigidBody2D', {\n velocityX: 0, velocityY: 0,\n mass: 1, gravityScale: 1,\n isKinematic: false, restitution: 0,\n});",
524
576
  codeValidation: {
525
577
  containsAll: ['createEntity', 'Transform', 'Collider2D', 'RigidBody2D'],
@@ -544,7 +596,7 @@ const TUTORIAL_PLATFORMER = {
544
596
  pl: 'Ruch w platformerze używa przyspieszenia i hamowania dla płynnego wrażenia. Sprawdź akcje `moveRight` i `moveLeft` i przyłóż prędkość.',
545
597
  de: 'Platformer-Bewegung nutzt Beschleunigung und Verzögerung für ein flüssiges Gefühl. Prüfe `moveRight` und `moveLeft` Aktionen und wende Geschwindigkeit an.',
546
598
  },
547
- codeTemplate: "// Movement system\nconst MOVE_SPEED = 200;\nconst ACCEL = 800;\nconst FRICTION = 600;\n",
599
+ codeTemplate: '// Movement system\nconst MOVE_SPEED = 200;\nconst ACCEL = 800;\nconst FRICTION = 600;\n',
548
600
  codeSolution: "const MOVE_SPEED = 200;\nconst ACCEL = 800;\nconst FRICTION = 600;\n\nfunction updateMovement(input: InputManager, rb: any, dt: number) {\n let moveDir = 0;\n if (input.getAction('moveRight')) moveDir += 1;\n if (input.getAction('moveLeft')) moveDir -= 1;\n\n if (moveDir !== 0) {\n // Accelerate toward target speed\n rb.velocityX += moveDir * ACCEL * dt;\n // Clamp to max speed\n if (Math.abs(rb.velocityX) > MOVE_SPEED) {\n rb.velocityX = Math.sign(rb.velocityX) * MOVE_SPEED;\n }\n } else {\n // Apply friction\n if (Math.abs(rb.velocityX) > FRICTION * dt) {\n rb.velocityX -= Math.sign(rb.velocityX) * FRICTION * dt;\n } else {\n rb.velocityX = 0;\n }\n }\n}",
549
601
  codeValidation: {
550
602
  containsAll: ['ACCEL', 'FRICTION', 'velocityX', 'getAction'],
@@ -569,7 +621,7 @@ const TUTORIAL_PLATFORMER = {
569
621
  pl: 'Coyote time pozwala graczowi skoczyć chwilę po opuszczeniu platformy — co sprawia, że gra jest bardziej wybaczalna. Śledź czas ostatniego kontaktu z podłożem.',
570
622
  de: 'Coyote-Time erlaubt dem Spieler kurz nach Verlassen einer Plattform zu springen — das Spiel fühlt sich nachsichtiger an. Verfolge die letzte Bodenberührungs-Zeit.',
571
623
  },
572
- codeTemplate: "// Jump system with coyote time\nconst JUMP_FORCE = -350;\nconst COYOTE_TIME = 0.1; // 100ms grace period\n",
624
+ codeTemplate: '// Jump system with coyote time\nconst JUMP_FORCE = -350;\nconst COYOTE_TIME = 0.1; // 100ms grace period\n',
573
625
  codeSolution: "const JUMP_FORCE = -350;\nconst COYOTE_TIME = 0.1; // 100ms grace period\nlet lastGroundedTime = 0;\nlet hasJumped = false;\n\nfunction updateJump(input: InputManager, rb: any, isGrounded: boolean, time: number) {\n if (isGrounded) {\n lastGroundedTime = time;\n hasJumped = false;\n }\n\n const canJump = !hasJumped && (time - lastGroundedTime) < COYOTE_TIME;\n\n if (input.getAction('jump') && canJump) {\n rb.velocityY = JUMP_FORCE;\n hasJumped = true;\n }\n\n // Variable jump height — release early = lower jump\n if (!input.getAction('jump') && rb.velocityY < 0) {\n rb.velocityY *= 0.5;\n }\n}",
574
626
  codeValidation: {
575
627
  containsAll: ['COYOTE_TIME', 'JUMP_FORCE', 'lastGroundedTime', 'canJump'],
@@ -594,7 +646,7 @@ const TUTORIAL_PLATFORMER = {
594
646
  pl: 'Dodaj dodatkowy skok w powietrzu (podwójny skok) i pozwól graczowi powoli zsuwać się po ścianach gdy ich dotyka.',
595
647
  de: 'Füge einen zusätzlichen Sprung in der Luft (Doppelsprung) hinzu und lass den Spieler langsam an Wänden herunterrutschen.',
596
648
  },
597
- codeTemplate: "// Double jump + wall slide\nconst MAX_AIR_JUMPS = 1;\nconst WALL_SLIDE_SPEED = 50;\n",
649
+ codeTemplate: '// Double jump + wall slide\nconst MAX_AIR_JUMPS = 1;\nconst WALL_SLIDE_SPEED = 50;\n',
598
650
  codeSolution: "const MAX_AIR_JUMPS = 1;\nconst WALL_SLIDE_SPEED = 50;\nlet airJumpsLeft = MAX_AIR_JUMPS;\n\nfunction updateAirMoves(input: InputManager, rb: any, isGrounded: boolean, isTouchingWall: boolean) {\n if (isGrounded) {\n airJumpsLeft = MAX_AIR_JUMPS;\n }\n\n // Double jump (only when not grounded)\n if (input.getAction('jump') && !isGrounded && airJumpsLeft > 0) {\n rb.velocityY = JUMP_FORCE;\n airJumpsLeft--;\n }\n\n // Wall slide — slow down fall when touching wall\n if (isTouchingWall && !isGrounded && rb.velocityY > 0) {\n rb.velocityY = Math.min(rb.velocityY, WALL_SLIDE_SPEED);\n }\n}",
599
651
  codeValidation: {
600
652
  containsAll: ['MAX_AIR_JUMPS', 'WALL_SLIDE_SPEED', 'airJumpsLeft'],
@@ -619,7 +671,7 @@ const TUTORIAL_PLATFORMER = {
619
671
  pl: 'Dash daje graczowi nagły przypływ prędkości w aktualnym kierunku. Dodaj cooldown, żeby nie spamować.',
620
672
  de: 'Ein Dash gibt dem Spieler einen Geschwindigkeitsschub in die aktuelle Richtung. Füge einen Cooldown hinzu.',
621
673
  },
622
- codeTemplate: "// Dash system\nconst DASH_SPEED = 500;\nconst DASH_DURATION = 0.15;\nconst DASH_COOLDOWN = 0.5;\n",
674
+ codeTemplate: '// Dash system\nconst DASH_SPEED = 500;\nconst DASH_DURATION = 0.15;\nconst DASH_COOLDOWN = 0.5;\n',
623
675
  codeSolution: "const DASH_SPEED = 500;\nconst DASH_DURATION = 0.15;\nconst DASH_COOLDOWN = 0.5;\nlet dashTimer = 0;\nlet dashCooldown = 0;\nlet dashDir = 1;\n\nfunction updateDash(input: InputManager, rb: any, dt: number, facingRight: boolean) {\n dashCooldown = Math.max(0, dashCooldown - dt);\n\n if (input.getAction('dash') && dashCooldown <= 0 && dashTimer <= 0) {\n dashTimer = DASH_DURATION;\n dashCooldown = DASH_COOLDOWN;\n dashDir = facingRight ? 1 : -1;\n }\n\n if (dashTimer > 0) {\n dashTimer -= dt;\n rb.velocityX = DASH_SPEED * dashDir;\n rb.velocityY = 0; // freeze vertical during dash\n }\n}",
624
676
  codeValidation: {
625
677
  containsAll: ['DASH_SPEED', 'DASH_COOLDOWN', 'dashTimer'],
@@ -644,7 +696,7 @@ const TUTORIAL_PLATFORMER = {
644
696
  pl: 'Utwórz wrogów patrolujących między dwoma punktami. Gdy gracz się zbliży, przechodzą w tryb pościgu!',
645
697
  de: 'Erstelle Feinde, die zwischen zwei Punkten patrouillieren. Wenn der Spieler nah kommt, wechseln sie in den Verfolgungsmodus!',
646
698
  },
647
- codeTemplate: "// Enemy patrol + chase AI\nconst PATROL_SPEED = 60;\nconst CHASE_SPEED = 120;\nconst DETECT_RANGE = 150;\n",
699
+ codeTemplate: '// Enemy patrol + chase AI\nconst PATROL_SPEED = 60;\nconst CHASE_SPEED = 120;\nconst DETECT_RANGE = 150;\n',
648
700
  codeSolution: "const PATROL_SPEED = 60;\nconst CHASE_SPEED = 120;\nconst DETECT_RANGE = 150;\n\nfunction updateEnemy(enemy: any, playerPos: any, dt: number) {\n const dx = playerPos.x - enemy.x;\n const dy = playerPos.y - enemy.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n if (dist < DETECT_RANGE) {\n // Chase mode — move toward player\n const dir = dx > 0 ? 1 : -1;\n enemy.velocityX = CHASE_SPEED * dir;\n enemy.state = 'chase';\n } else {\n // Patrol mode — walk between patrolLeft and patrolRight\n if (enemy.x <= enemy.patrolLeft) enemy.patrolDir = 1;\n if (enemy.x >= enemy.patrolRight) enemy.patrolDir = -1;\n enemy.velocityX = PATROL_SPEED * enemy.patrolDir;\n enemy.state = 'patrol';\n }\n}",
649
701
  codeValidation: {
650
702
  containsAll: ['PATROL_SPEED', 'CHASE_SPEED', 'DETECT_RANGE'],
@@ -676,7 +728,7 @@ const TUTORIAL_PLATFORMER = {
676
728
  containsAll: ['createEntity', 'isTrigger', 'destroyEntity'],
677
729
  },
678
730
  hint: {
679
- en: 'Set isTrigger: true on coins so they don\'t push the player',
731
+ en: "Set isTrigger: true on coins so they don't push the player",
680
732
  pl: 'Ustaw isTrigger: true na monetach, aby nie odpychały gracza',
681
733
  de: 'Setze isTrigger: true auf Münzen, damit sie den Spieler nicht wegstoßen',
682
734
  },
@@ -695,7 +747,7 @@ const TUTORIAL_PLATFORMER = {
695
747
  pl: 'Użyj `Tilemap` do zbudowania poziomu. Zdefiniuj podłoże, platformy i ściany z kafelków.',
696
748
  de: 'Verwende eine `Tilemap` um das Level zu bauen. Definiere Boden, Plattformen und Wände mit Kacheln.',
697
749
  },
698
- codeTemplate: "// Create the level tilemap\nconst level = new Tilemap(20, 15, 32);\n",
750
+ codeTemplate: '// Create the level tilemap\nconst level = new Tilemap(20, 15, 32);\n',
699
751
  codeSolution: "const level = new Tilemap(20, 15, 32);\n\n// Add a ground layer\nlevel.addLayer('ground', 20, 15);\n\n// Fill the ground row\nlevel.fillRect('ground', 0, 14, 20, 1, 1); // bottom row = solid\n\n// Add some platforms\nlevel.setTile('ground', 5, 10, 1); // floating block\nlevel.setTile('ground', 6, 10, 1);\nlevel.setTile('ground', 7, 10, 1);\n\nlevel.fillRect('ground', 12, 8, 4, 1, 1); // higher platform\n\n// Check collisions with tilemap\nconst isSolid = level.isSolid(5, 14); // true — ground!",
700
752
  codeValidation: {
701
753
  containsAll: ['Tilemap', 'addLayer', 'fillRect'],
@@ -716,7 +768,7 @@ const TUTORIAL_PLATFORMER = {
716
768
  de: 'Platformer fertig! 🎮',
717
769
  },
718
770
  content: {
719
- en: 'Congratulations! You\'ve built a complete platformer with:\n• Smooth acceleration-based movement\n• Jumping with coyote time\n• Double jump & wall slide\n• Dash mechanic\n• Enemy AI (patrol & chase)\n• Coins with trigger colliders\n• Tilemap level design\n\nChallenge: Add moving platforms and a boss fight!',
771
+ en: "Congratulations! You've built a complete platformer with:\n• Smooth acceleration-based movement\n• Jumping with coyote time\n• Double jump & wall slide\n• Dash mechanic\n• Enemy AI (patrol & chase)\n• Coins with trigger colliders\n• Tilemap level design\n\nChallenge: Add moving platforms and a boss fight!",
720
772
  pl: 'Gratulacje! Zbudowałeś kompletnego platformera z:\n• Płynny ruch z przyspieszeniem\n• Skakanie z coyote time\n• Podwójny skok i wall-slide\n• Mechanika dashu\n• AI wrogów (patrol i pościg)\n• Monety z trigger koliderami\n• Design poziomu z tilemapy\n\nWyzwanie: Dodaj ruchome platformy i walkę z bossem!',
721
773
  de: 'Herzlichen Glückwunsch! Du hast einen kompletten Platformer gebaut mit:\n• Flüssige beschleunigungsbasierte Bewegung\n• Springen mit Coyote-Time\n• Doppelsprung & Wall-Slide\n• Dash-Mechanik\n• Feind-KI (Patrouille & Verfolgung)\n• Münzen mit Trigger-Kollidern\n• Tilemap Level-Design\n\nHerausforderung: Füge bewegliche Plattformen und einen Bosskampf hinzu!',
722
774
  },
@@ -727,11 +779,7 @@ const TUTORIAL_PLATFORMER = {
727
779
  Convenience export
728
780
  ══════════════════════════════════════════════════════════════ */
729
781
  function getTutorials_T09_T11() {
730
- return [
731
- TUTORIAL_MULTIPLAYER_ONLINE,
732
- TUTORIAL_PROCEDURAL_WORLD,
733
- TUTORIAL_PLATFORMER,
734
- ];
782
+ return [TUTORIAL_MULTIPLAYER_ONLINE, TUTORIAL_PROCEDURAL_WORLD, TUTORIAL_PLATFORMER];
735
783
  }
736
784
 
737
785
  exports.TUTORIAL_MULTIPLAYER_ONLINE = TUTORIAL_MULTIPLAYER_ONLINE;