@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 +48 -0
- package/dist/cjs/core/GameConfig.js +21 -7
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/release/MobileExport.js +45 -12
- package/dist/cjs/tutorials/content/Tutorials_T05_T08.js +33 -16
- package/dist/cjs/tutorials/content/Tutorials_T09_T11.js +79 -31
- package/dist/cjs/tutorials/content/Tutorials_T12_T15.js +46 -23
- package/dist/esm/core/GameConfig.js +21 -7
- package/dist/esm/release/MobileExport.js +45 -12
- package/dist/esm/tutorials/content/Tutorials_T05_T08.js +33 -16
- package/dist/esm/tutorials/content/Tutorials_T09_T11.js +79 -31
- package/dist/esm/tutorials/content/Tutorials_T12_T15.js +46 -23
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/input/index.d.ts +1 -1
- package/dist/types/legendary/index.d.ts +7 -7
- package/dist/types/procedural/index.d.ts +1 -1
- package/package.json +2 -2
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() {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
get
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
get
|
|
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
|
}
|
|
@@ -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
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
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',
|
|
128
|
-
|
|
129
|
-
|
|
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',
|
|
133
|
-
|
|
134
|
-
|
|
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',
|
|
138
|
-
|
|
139
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
45
|
-
pl:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
{
|
|
531
|
-
|
|
532
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
773
|
-
codeSolution:
|
|
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:
|
|
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:
|
|
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
|
-
{
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
{
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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;
|