Xenova HF Staff commited on
Commit
846e338
·
verified ·
1 Parent(s): 38c32ac

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1428 -18
index.html CHANGED
@@ -1,19 +1,1429 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>FunctionGemma Physics Playground</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/decomp.min.js"></script>
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet" />
12
+ <link
13
+ href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;700&family=Fira+Code:wght@500;700&display=swap"
14
+ rel="stylesheet" />
15
+ <script>
16
+ tailwind.config = {
17
+ theme: {
18
+ extend: {
19
+ fontFamily: {
20
+ sans: ["Space Grotesk", "sans-serif"],
21
+ mono: ["Fira Code", "monospace"],
22
+ },
23
+ colors: {
24
+ "neo-bg": "#f0f0f0",
25
+ "neo-black": "#1a1a1a",
26
+ "neo-white": "#ffffff",
27
+ "neo-purple": "#a78bfa",
28
+ "neo-yellow": "#facc15",
29
+ "neo-green": "#4ade80",
30
+ "neo-red": "#f87171",
31
+ "neo-blue": "#60a5fa",
32
+ "neo-gray": "#94a3b8",
33
+ },
34
+ boxShadow: {
35
+ neo: "4px 4px 0px 0px #000000",
36
+ "neo-sm": "2px 2px 0px 0px #000000",
37
+ "neo-lg": "8px 8px 0px 0px #000000",
38
+ },
39
+ },
40
+ },
41
+ };
42
+ </script>
43
+
44
+ <style>
45
+ body {
46
+ overflow: hidden;
47
+ background-color: #e0e7ff;
48
+ background-image: radial-gradient(#a5b4fc 1px, transparent 1px);
49
+ background-size: 20px 20px;
50
+ }
51
+
52
+ /* Custom Scrollbar */
53
+ ::-webkit-scrollbar {
54
+ width: 12px;
55
+ height: 12px;
56
+ }
57
+
58
+ ::-webkit-scrollbar-track {
59
+ background: #fff;
60
+ border-left: 2px solid black;
61
+ }
62
+
63
+ ::-webkit-scrollbar-thumb {
64
+ background: #000;
65
+ border: 2px solid #fff;
66
+ }
67
+
68
+ ::-webkit-scrollbar-thumb:hover {
69
+ background: #333;
70
+ }
71
+
72
+ .neo-border {
73
+ border: 3px solid black;
74
+ }
75
+
76
+ .neo-btn {
77
+ transition: all 0.1s ease-in-out;
78
+ }
79
+
80
+ .neo-btn:active {
81
+ transform: translate(2px, 2px);
82
+ box-shadow: 2px 2px 0px 0px #000000;
83
+ }
84
+
85
+ .level-card {
86
+ transition: all 0.2s;
87
+ }
88
+
89
+ .level-card:hover:not(.locked) {
90
+ transform: translate(-2px, -2px);
91
+ box-shadow: 6px 6px 0px 0px #000000;
92
+ }
93
+
94
+ .locked {
95
+ background-color: #e2e8f0;
96
+ cursor: not-allowed;
97
+ opacity: 0.7;
98
+ background-image: repeating-linear-gradient(45deg, #cbd5e1 0, #cbd5e1 1px, transparent 0, transparent 50%);
99
+ background-size: 10px 10px;
100
+ }
101
+
102
+ .code-editor {
103
+ font-family: "Fira Code", monospace;
104
+ background-color: #ffffff;
105
+ color: #000000;
106
+ line-height: 1.6;
107
+ }
108
+
109
+ .toggle-checkbox:checked {
110
+ right: 0;
111
+ border-color: #4ade80;
112
+ }
113
+
114
+ .toggle-checkbox:checked+.toggle-label {
115
+ background-color: #4ade80;
116
+ }
117
+
118
+ /* Action Item Delete Button Transition */
119
+ .action-item .btn-delete {
120
+ opacity: 0;
121
+ transition: opacity 0.2s;
122
+ }
123
+
124
+ .action-item:hover .btn-delete {
125
+ opacity: 1;
126
+ }
127
+ </style>
128
+ </head>
129
+
130
+ <body class="h-screen w-screen flex flex-col md:flex-row p-4 gap-4">
131
+ <!-- Loading Overlay -->
132
+ <div id="loading-overlay" class="fixed inset-0 bg-black/90 z-50 flex flex-col items-center justify-center">
133
+ <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-neo-green mb-4"></div>
134
+ <div class="text-white font-mono font-bold text-xl">LOADING MODEL...</div>
135
+ <div class="text-gray-400 font-mono text-sm mt-2">This may take a moment</div>
136
+ </div>
137
+
138
+ <!-- Sidebar / Controls -->
139
+ <div class="w-full md:w-1/3 lg:w-1/4 flex flex-col neo-border shadow-neo bg-white h-full z-10 relative">
140
+ <!-- Decorative Header Strip -->
141
+ <div class="h-4 w-full bg-neo-black"></div>
142
+
143
+ <div class="p-6 border-b-2 border-black bg-neo-yellow flex justify-between items-center shrink-0">
144
+ <div>
145
+ <h1 class="text-3xl font-bold text-black uppercase tracking-tighter">Function <span
146
+ class="text-white bg-black px-1">Gemma</span></h1>
147
+ <div class="text-xs font-mono font-bold text-black mt-1">PHYSICS PLAYGROUND</div>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- MENU: Level Select -->
152
+ <div id="view-menu" class="flex-1 overflow-y-auto p-6 bg-white hidden">
153
+ <h2 class="font-bold text-xl mb-4 border-b-2 border-black pb-2">SELECT LEVEL</h2>
154
+ <div id="level-grid" class="grid grid-cols-2 gap-4">
155
+ <!-- Injected via JS -->
156
+ </div>
157
+ </div>
158
+
159
+ <!-- MENU: Game View -->
160
+ <div id="view-game" class="flex-1 flex flex-col overflow-hidden relative">
161
+ <div class="overflow-y-auto flex-1 p-6 flex flex-col gap-6">
162
+ <!-- Level Header -->
163
+ <div>
164
+ <div class="flex items-center gap-2 mb-2">
165
+ <button id="btn-back"
166
+ class="neo-btn p-2 border-2 border-black shadow-neo-sm bg-white hover:bg-gray-100 text-xs font-bold"><i
167
+ class="fas fa-arrow-left"></i> LEVELS</button>
168
+ <div class="font-bold text-sm bg-black text-white px-2 py-1 ml-auto" id="level-indicator">LEVEL 1</div>
169
+ </div>
170
+
171
+ <!-- Level Info Card -->
172
+ <div class="neo-border p-4 bg-neo-blue shadow-neo-sm relative overflow-hidden group">
173
+ <div
174
+ class="absolute -right-4 -top-4 w-16 h-16 bg-white/20 rounded-full group-hover:scale-150 transition-transform">
175
+ </div>
176
+ <div class="flex justify-between items-center mb-3 relative z-10">
177
+ <h2 class="font-bold text-black text-lg border-b-2 border-black inline-block bg-white px-2"
178
+ id="level-title">...</h2>
179
+ <span id="timer-display"
180
+ class="font-mono text-xl font-bold bg-black text-neo-green px-2 py-0.5 border-2 border-black shadow-[2px_2px_0px_0px_#fff]">0.00s</span>
181
+ </div>
182
+ <p class="text-sm text-black font-medium opacity-90 mb-2" id="level-desc">...</p>
183
+
184
+ <!-- Collapsible Hint -->
185
+ <div class="mt-2">
186
+ <button id="btn-hint"
187
+ class="text-xs font-bold border-2 border-black px-2 py-1 bg-white hover:bg-yellow-100 shadow-neo-sm flex items-center gap-2"><i
188
+ class="fas fa-lightbulb text-yellow-500"></i> SHOW HINT</button>
189
+ <div id="hint-content" class="mt-2 bg-white/50 p-2 border-2 border-black text-xs font-mono hidden"><i
190
+ class="fas fa-code mr-1"></i> <span id="level-hint-text">...</span></div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ <!-- Editor / Command Input -->
196
+ <div class="flex flex-col gap-2 shrink-0">
197
+ <div class="flex justify-between items-end">
198
+ <div class="flex gap-2 items-center">
199
+ <label class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm">COMMAND</label>
200
+ <button id="btn-solution"
201
+ class="text-xs font-bold border-2 border-black px-2 py-0.5 hover:bg-black hover:text-white transition-colors bg-white shadow-neo-sm"
202
+ title="Load Example Solution"><i class="fas fa-magic"></i> VIEW SOLUTION</button>
203
+ </div>
204
+ <div class="text-xs font-bold text-gray-500" id="star-reqs">3★ < 2 items</div>
205
+ </div>
206
+ <div class="relative flex flex-col gap-2">
207
+ <textarea id="code-input"
208
+ class="code-editor w-full h-24 p-4 border-2 border-black focus:outline-none focus:ring-4 focus:ring-neo-purple/50 resize-none text-sm shadow-neo-sm"
209
+ spellcheck="false" placeholder="e.g., Add a circle in the middle. You can execute multiple commands by separating them with new lines."></textarea>
210
+ <button id="btn-execute"
211
+ class="neo-btn bg-neo-purple border-2 border-black shadow-neo-sm text-black font-bold py-2 hover:bg-purple-400 disabled:opacity-50 disabled:cursor-not-allowed"><i
212
+ class="fas fa-terminal"></i> EXECUTE</button>
213
+ </div>
214
+ <div id="error-log"
215
+ class="text-white bg-neo-red border-2 border-black text-xs font-bold px-2 py-1 shadow-neo-sm hidden">
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Active Elements List -->
220
+ <div class="flex-1 min-h-0 flex flex-col">
221
+ <label class="text-sm font-bold bg-black text-white px-2 py-0.5 shadow-neo-sm w-max mb-1">SCENE
222
+ OBJECTS</label>
223
+ <div id="action-list"
224
+ class="flex-1 overflow-y-auto border-2 border-black bg-gray-50 p-2 space-y-2 shadow-neo-sm min-h-[100px]">
225
+ <div class="text-xs text-gray-400 text-center mt-4 italic">No elements added yet.</div>
226
+ </div>
227
+ </div>
228
+ </div>
229
+
230
+ <!-- Action Bar -->
231
+ <div class="p-4 bg-gray-100 border-t-2 border-black flex gap-3 shrink-0">
232
+ <button id="btn-play"
233
+ class="neo-btn flex-1 bg-neo-green border-2 border-black shadow-neo text-black font-bold py-3 px-4 flex items-center justify-center gap-2 hover:bg-green-400"><i
234
+ class="fas fa-play"></i> RUN</button>
235
+ <button id="btn-reset"
236
+ class="neo-btn px-4 py-3 bg-white border-2 border-black shadow-neo text-black font-bold hover:bg-gray-100"
237
+ title="Reset Simulation">
238
+ <i class="fas fa-undo"></i>
239
+ </button>
240
+ <button id="btn-clear-all"
241
+ class="neo-btn px-4 py-3 bg-white border-2 border-black shadow-neo text-black font-bold hover:bg-neo-red hover:text-white transition-colors"
242
+ title="Clear All Objects">
243
+ <i class="fas fa-trash-alt"></i>
244
+ </button>
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- Main Canvas Area -->
250
+ <div class="flex-1 relative flex items-center justify-center p-2">
251
+ <!-- Canvas Container -->
252
+ <div id="canvas-container"
253
+ class="w-full h-full bg-white neo-border shadow-neo flex justify-center items-center relative overflow-hidden">
254
+ <!-- Win Overlay -->
255
+ <div id="win-message"
256
+ class="absolute z-50 hidden w-full h-full flex items-center justify-center bg-black/50 backdrop-blur-sm">
257
+ <div
258
+ class="bg-neo-yellow border-4 border-black p-8 shadow-neo-lg text-center transform rotate-2 max-w-md w-full m-4">
259
+ <h2 class="text-4xl font-black mb-2 text-black">LEVEL CLEAR!</h2>
260
+
261
+ <div class="flex justify-center gap-2 mb-4 text-4xl text-white drop-shadow-md" id="result-stars">
262
+ <!-- Stars injected here -->
263
+ </div>
264
+
265
+ <div class="text-sm font-bold mb-6 font-mono">ITEMS USED: <span id="result-items">0</span></div>
266
+
267
+ <div class="flex flex-col gap-2">
268
+ <button id="btn-next-level"
269
+ class="bg-black text-white font-bold py-3 px-6 border-2 border-transparent hover:bg-neo-green hover:text-black hover:border-black transition-colors shadow-neo-sm">NEXT
270
+ LEVEL <i class="fas fa-arrow-right ml-2"></i></button>
271
+ <button onclick="document.getElementById('btn-reset').click()"
272
+ class="bg-white text-black font-bold py-2 px-6 border-2 border-black hover:bg-gray-100 transition-colors shadow-neo-sm">REPLAY</button>
273
+ </div>
274
+ </div>
275
+ </div>
276
+
277
+ <!-- Canvas Injected Here -->
278
+ </div>
279
+
280
+ <!-- Status Badge -->
281
+ <div class="absolute top-6 right-8 pointer-events-none z-20">
282
+ <div id="status-badge"
283
+ class="bg-white border-2 border-black shadow-neo-sm px-4 py-2 font-black text-sm uppercase tracking-widest flex items-center gap-2">
284
+ <div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div>
285
+ READY
286
+ </div>
287
+ </div>
288
+ </div>
289
+ <script type="module">
290
+ import { AutoModelForCausalLM, AutoTokenizer } from "https://cdn.jsdelivr.net/npm/@huggingface/[email protected]";
291
+
292
+ // --- Game Constants ---
293
+ const CONFIG = {
294
+ width: 1000,
295
+ height: 750,
296
+ gridWidth: 20,
297
+ gridHeight: 15,
298
+ colors: {
299
+ background: "#ffffff",
300
+ wall: "#1a1a1a",
301
+ ball: "#60a5fa",
302
+ goal: "#4ade80",
303
+ userShape: "#facc15",
304
+ },
305
+ };
306
+
307
+ const STORAGE_KEY = "functiongemma_save_v1";
308
+
309
+ // --- Helper Functions ---
310
+ const pX = (units) => (units * CONFIG.width) / CONFIG.gridWidth;
311
+ const pY = (units) => (units * CONFIG.height) / CONFIG.gridHeight;
312
+ const strokeStyle = { strokeStyle: "#000000", lineWidth: 3 };
313
+
314
+ // --- LEVEL DEFINITIONS ---
315
+ const LEVELS = [
316
+ {
317
+ id: 0,
318
+ title: "Tutorial",
319
+ difficulty: 1,
320
+ desc: "Welcome! Press RUN to start the simulation. The ball will move on its own.",
321
+ hint: "Just press the green RUN button!",
322
+ stars: [0, 1],
323
+ solution: `// No code needed!`,
324
+ setup: (World, Bodies, Composite) => {
325
+ const floor = Bodies.rectangle(CONFIG.width / 2, CONFIG.height + 25, CONFIG.width, 100, { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
326
+
327
+ const ball = Bodies.circle(pX(2), CONFIG.height - 50 - 20, 20, {
328
+ restitution: 0.6,
329
+ friction: 0,
330
+ frictionAir: 0,
331
+ frictionStatic: 0,
332
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
333
+ });
334
+
335
+ Matter.Body.setVelocity(ball, { x: 10, y: 0 });
336
+
337
+ const goal = Bodies.rectangle(pX(18), CONFIG.height - 50 - 60, 100, 120, {
338
+ isStatic: true,
339
+ isSensor: true,
340
+ label: "GoalZone",
341
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle, lineWidth: 2, strokeStyle: "#000" },
342
+ });
343
+
344
+ World.add(Composite, [floor, ball, goal]);
345
+ return { ball, goal };
346
+ },
347
+ },
348
+ {
349
+ id: 1,
350
+ title: "The Bridge",
351
+ difficulty: 1,
352
+ desc: "There is a gap in the path. Build a bridge so the ball can roll across.",
353
+ hint: "Add a wide line in the center to connect the platforms.",
354
+ stars: [1, 2],
355
+ solution: `add a long line in the middle`,
356
+ setup: (World, Bodies, Composite) => {
357
+ const p1 = Bodies.rectangle(pX(4), pY(4), pX(6), 20, { isStatic: true, angle: Math.PI / 8, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
358
+ const p2 = Bodies.rectangle(pX(16), pY(12), pX(6), 20, { isStatic: true, angle: Math.PI / 8, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
359
+
360
+ const ball = Bodies.circle(pX(3), pY(2), 20, {
361
+ restitution: 0.2,
362
+ friction: 0,
363
+ frictionAir: 0,
364
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
365
+ });
366
+
367
+ const goal = Bodies.rectangle(pX(18), pY(11), 80, 80, {
368
+ isStatic: true,
369
+ isSensor: true,
370
+ label: "GoalZone",
371
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
372
+ });
373
+
374
+ World.add(Composite, [p1, p2, ball, goal]);
375
+ return { ball, goal };
376
+ },
377
+ },
378
+ {
379
+ id: 2,
380
+ title: "A Little Push",
381
+ difficulty: 2,
382
+ desc: "Oh no, we're stuck! Give the ball a nudge to get it moving towards the goal.",
383
+ hint: "Drop a heavy object in the top left to push it towards the goal.",
384
+ stars: [1, 3],
385
+ solution: `Add a circle at 2,2`,
386
+ setup: (World, Bodies, Composite) => {
387
+ const floor = Bodies.rectangle(CONFIG.width / 2, CONFIG.height + 25, CONFIG.width, 100, { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
388
+
389
+ const ball = Bodies.circle(pX(2.2), CONFIG.height - 50 - 20, 20, {
390
+ restitution: 0.6,
391
+ friction: 0,
392
+ frictionAir: 0,
393
+ frictionStatic: 0,
394
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
395
+ });
396
+
397
+ const goal = Bodies.rectangle(pX(18), CONFIG.height - 50 - 60, 100, 120, {
398
+ isStatic: true,
399
+ isSensor: true,
400
+ label: "GoalZone",
401
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle, lineWidth: 2, strokeStyle: "#000" },
402
+ });
403
+
404
+ World.add(Composite, [floor, ball, goal]);
405
+ return { ball, goal };
406
+ },
407
+ },
408
+ {
409
+ id: 3,
410
+ title: "The Bounce",
411
+ difficulty: 2,
412
+ desc: "High velocity incoming! How can you redirect the ball into the goal?",
413
+ hint: "Add a centered platform at the bottom to reflect the ball towards the goal.",
414
+ stars: [1, 2],
415
+ solution: `add a wide line at the bottom`,
416
+ setup: (World, Bodies, Composite) => {
417
+ // Wall in middle top
418
+ const wall = Bodies.rectangle(pX(10), pY(3), 20, pY(6), { isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } });
419
+
420
+ const ball = Bodies.circle(pX(2), pY(1.2), 20, {
421
+ restitution: 1.0,
422
+ friction: 0,
423
+ frictionAir: 0.001,
424
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
425
+ });
426
+
427
+ // Initial Velocity: Down and Right
428
+ Matter.Body.setVelocity(ball, { x: 12, y: 12 });
429
+
430
+ const goal = Bodies.rectangle(pX(18), pY(2), pX(2), pY(2), {
431
+ isStatic: true,
432
+ isSensor: true,
433
+ label: "GoalZone",
434
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
435
+ });
436
+
437
+ World.add(Composite, [wall, ball, goal]);
438
+ return { ball, goal };
439
+ },
440
+ },
441
+ {
442
+ id: 4,
443
+ title: "Protect",
444
+ difficulty: 3,
445
+ desc: "Ambush! Ninja stars are incoming. Protect the ball's path so it can land safely.",
446
+ hint: "Add a few heavy blocks at certain locations to shield the ball from the stars.",
447
+ stars: [3, 5],
448
+ solution: `add a heavy block at 5, 3\nadd a heavy block at 15, 5\nadd a heavy block at 5, 7`,
449
+ setup: (World, Bodies, Composite, addEvent) => {
450
+ const ball = Bodies.circle(pX(10), pY(1), 20, {
451
+ restitution: 0.5,
452
+ friction: 0.001,
453
+ frictionAir: 0.001,
454
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
455
+ });
456
+
457
+ const goal = Bodies.rectangle(pX(10), pY(14), pX(2), pY(2), {
458
+ isStatic: true,
459
+ isSensor: true,
460
+ label: "GoalZone",
461
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
462
+ });
463
+
464
+ const createNinjaStar = (x, y, vx, vy, angVel, delay) => {
465
+ const starVerts = Matter.Vertices.fromPath('50 0 63 38 100 38 69 59 82 100 50 75 18 100 31 59 0 38 37 38');
466
+ const star = Matter.Bodies.fromVertices(x, y, starVerts, {
467
+ render: { fillStyle: "#ef4444", strokeStyle: "#ef4444", lineWidth: 1 },
468
+ restitution: 0.8,
469
+ isStatic: true
470
+ }, true);
471
+ Matter.Body.scale(star, 0.5, 0.5);
472
+ Matter.Body.setVelocity(star, { x: vx, y: vy });
473
+ Matter.Body.setAngularVelocity(star, angVel);
474
+
475
+ World.add(Composite, star);
476
+
477
+ if (addEvent) {
478
+ addEvent(delay, () => {
479
+ Matter.Body.setStatic(star, false);
480
+ });
481
+ }
482
+ return star;
483
+ };
484
+
485
+ // Ninja Stars
486
+ createNinjaStar(pX(2), pY(5), 15, -5, 0.3, 0.1);
487
+ createNinjaStar(pX(18), pY(8), -15, -5, -0.3, 0.3);
488
+ createNinjaStar(pX(2), pY(11), 15, -5, 0.3, 0.50);
489
+
490
+ World.add(Composite, [ball, goal]);
491
+ return { ball, goal };
492
+ },
493
+ },
494
+ {
495
+ id: 5,
496
+ title: "Timing is Key",
497
+ difficulty: 3,
498
+ desc: "The goal is moving! Push the ball off the ledge so it lands in the moving target.",
499
+ hint: "Add a heavy ball on the top left after a short delay to drop the ball onto the platform.",
500
+ stars: [1, 2],
501
+ solution: `add a heavy ball in the top left after 1 second`,
502
+ setup: (World, Bodies, Composite) => {
503
+ // Rotating Cross
504
+ const cross = Matter.Body.create({
505
+ parts: [
506
+ Bodies.rectangle(pX(8), pY(4), pX(7), 10, { render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } }),
507
+ Bodies.rectangle(pX(8), pY(4), 10, pX(7), { render: { fillStyle: CONFIG.colors.wall, ...strokeStyle } })
508
+ ],
509
+ isStatic: true,
510
+ });
511
+
512
+ Matter.Events.on(STATE.engine, 'beforeUpdate', () => {
513
+ if (STATE.isPlaying) {
514
+ Matter.Body.rotate(cross, -0.01);
515
+ }
516
+ });
517
+
518
+ World.add(Composite, cross);
519
+
520
+ // Platform
521
+ const platform = Bodies.rectangle(pX(8), pY(8)-10, pX(14), 20, {
522
+ isStatic: true,
523
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
524
+ });
525
+
526
+ // Ball
527
+ const ball = Bodies.circle(pX(2.1), pY(7), 20, {
528
+ restitution: 0.5,
529
+ friction: 0.1,
530
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
531
+ });
532
+
533
+ // Moving Goal
534
+ const goal = Bodies.rectangle(pX(10), pY(13), 120, 120, {
535
+ isStatic: true,
536
+ isSensor: true,
537
+ label: "GoalZone",
538
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
539
+ });
540
+
541
+ // Goal Movement Logic
542
+ const updateGoal = () => {
543
+ if (!STATE.isPlaying) return;
544
+ const time = STATE.time;
545
+ const speed = 0.002;
546
+ const range = pX(8);
547
+ const center = pX(10);
548
+ const x = center + Math.sin(time * speed) * range;
549
+ Matter.Body.setPosition(goal, { x: x, y: pY(13) });
550
+ };
551
+
552
+ Matter.Events.on(STATE.engine, 'beforeUpdate', updateGoal);
553
+
554
+ World.add(Composite, [platform, ball, goal]);
555
+ return { ball, goal };
556
+ },
557
+ },
558
+ {
559
+ id: 6,
560
+ title: "Catapult",
561
+ difficulty: 3,
562
+ desc: "Launch the ball into the goal! Can you time it right?",
563
+ hint: "Drop a heavy object in the right place at the right time to send the ball flying.",
564
+ stars: [1, 2],
565
+ solution: `add a heavy square at 14, 1, delayed by 2 seconds`,
566
+ setup: (World, Bodies, Composite) => {
567
+ const group = Matter.Body.nextGroup(true);
568
+
569
+ // Ramp for ball
570
+ const ramp = Bodies.rectangle(pX(2), pY(11.5), pX(4), 5, {
571
+ isStatic: true,
572
+ angle: Math.PI / 16,
573
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
574
+ });
575
+
576
+ // Catapult
577
+ const pivotX = pX(10);
578
+ const pivotY = pY(13);
579
+ const catapult = Bodies.rectangle(pivotX, pivotY - 10, pX(12), 10, {
580
+ collisionFilter: { group: group },
581
+ density: 0.0001,
582
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
583
+ });
584
+
585
+ const pivot = Bodies.rectangle(pivotX, pivotY + 20, 20, 60, {
586
+ isStatic: true,
587
+ collisionFilter: { group: group },
588
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
589
+ });
590
+
591
+ const constraint = Matter.Constraint.create({
592
+ bodyA: catapult,
593
+ pointB: { x: pivotX, y: pivotY - 10 },
594
+ stiffness: 1.0,
595
+ length: 0,
596
+ render: { visible: true }
597
+ });
598
+
599
+ // Ball
600
+ const ball = Bodies.circle(pX(1), pY(8), 20, {
601
+ restitution: 0.0,
602
+ friction: 0.0,
603
+ density: 0.00001,
604
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
605
+ });
606
+
607
+ // Goal
608
+ const goal = Bodies.rectangle(pX(18), pY(2), pX(4), pY(4), {
609
+ isStatic: true,
610
+ isSensor: true,
611
+ label: "GoalZone",
612
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
613
+ });
614
+
615
+ World.add(Composite, [ramp, catapult, pivot, constraint, ball, goal]);
616
+ return { ball, goal };
617
+ },
618
+ },
619
+ {
620
+ id: 7,
621
+ title: "Newton's Cradle",
622
+ difficulty: 4,
623
+ desc: "A chain reaction! The ball is part of a Newton's cradle. How can we get things moving?",
624
+ hint: "Add heavy circles on the left and right to start the motion. You may need to time them carefully.",
625
+ stars: [2, 4],
626
+ solution: `Add a heavy circle in the top left.\nAdd a heavy circle in the top right, delayed by 10 seconds.`,
627
+ setup: (World, Bodies, Composite) => {
628
+ const xx = pX(8), yy = pY(2), number = 5, size = 25, length = pY(4);
629
+
630
+ const separation = 2.1;
631
+ for (let i = 0; i < number; i++) {
632
+ const x = xx + i * (size * separation);
633
+ const circle = Bodies.circle(x, yy + length, size, {
634
+ inertia: Infinity, restitution: 0.1, friction: 0, frictionAir: 0, slop: size * 0.02,
635
+ render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
636
+ });
637
+ const constraint = Matter.Constraint.create({
638
+ pointA: { x: x, y: yy },
639
+ bodyB: circle,
640
+ stiffness: 1,
641
+ length: length,
642
+ render: { strokeStyle: '#000', lineWidth: 1 }
643
+ });
644
+ World.add(Composite, [circle, constraint]);
645
+ }
646
+
647
+ const ramp = Bodies.rectangle(pX(4), pY(5.5), pX(7), 10, {
648
+ friction: 0.01,
649
+ frictionStatic: 0.01,
650
+ restitution: 0.1,
651
+ isStatic: true, angle: Math.PI / 12, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
652
+ });
653
+
654
+ // Tunnel walls
655
+ const p1 = Bodies.rectangle(pX(14.5), yy + length + 30, pX(3), 10, {
656
+ isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
657
+ });
658
+ const p2 = Bodies.rectangle(pX(15.25), yy + length + 30 - pY(1) - 10, pX(4.5), 10, {
659
+ isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
660
+ });
661
+ const p3 = Bodies.rectangle(pX(17.85), pY(8), pX(4), 10, {
662
+ isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
663
+ });
664
+ const p4 = Bodies.rectangle(pX(19.75), pY(6.1), pX(4), 10, {
665
+ // make vertical
666
+ angle: Math.PI / 2,
667
+ restitution: 0.1,
668
+ isStatic: true, render: { fillStyle: CONFIG.colors.wall, ...strokeStyle }
669
+ });
670
+
671
+ const ball = Bodies.circle(xx + number * (size * separation), yy + length, 20, {
672
+ restitution: 0.1, friction: 0.0, frictionAir: 0.0,
673
+ render: { fillStyle: CONFIG.colors.ball, ...strokeStyle },
674
+ });
675
+ const goal = Bodies.rectangle(pX(6.5), pY(12.5), pX(8), pY(4), {
676
+ isStatic: true,
677
+ isSensor: true,
678
+ label: "GoalZone",
679
+ render: { fillStyle: CONFIG.colors.goal, opacity: 0.3, ...strokeStyle },
680
+ });
681
+
682
+ World.add(Composite, [ramp, p1, p2, p3, p4, ball, goal]);
683
+
684
+ return { ball, goal };
685
+ },
686
+ }
687
+ ];
688
+
689
+ // --- State Management ---
690
+ const STATE = {
691
+ engine: null,
692
+ render: null,
693
+ runner: null,
694
+ time: 0,
695
+ startTime: 0,
696
+ isPlaying: false,
697
+ isFinished: false,
698
+ eventQueue: [],
699
+ plannedActions: [],
700
+ currentLevelIndex: 0,
701
+ currentBall: null,
702
+ progress: { unlockedIndex: 0, stars: {} },
703
+ winCondition: null,
704
+ };
705
+
706
+ function loadProgress() {
707
+ const saved = localStorage.getItem(STORAGE_KEY);
708
+ if (saved) {
709
+ try {
710
+ STATE.progress = JSON.parse(saved);
711
+ } catch (e) {
712
+ console.error("Save Corrupt");
713
+ }
714
+ }
715
+ }
716
+ function saveProgress() {
717
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(STATE.progress));
718
+ }
719
+
720
+ // --- Engine Initialization ---
721
+ function initPhysics() {
722
+ const container = document.getElementById("canvas-container");
723
+
724
+ STATE.engine = Matter.Engine.create();
725
+ STATE.engine.world.gravity.y = 1;
726
+ STATE.engine.timing.timeScale = 0.5;
727
+
728
+ STATE.render = Matter.Render.create({
729
+ element: container,
730
+ engine: STATE.engine,
731
+ options: {
732
+ width: CONFIG.width,
733
+ height: CONFIG.height,
734
+ wireframes: false,
735
+ background: CONFIG.colors.background,
736
+ pixelRatio: window.devicePixelRatio,
737
+ showAngleIndicator: false,
738
+ },
739
+ });
740
+
741
+ Matter.Events.on(STATE.render, "afterRender", function () {
742
+ const context = STATE.render.context;
743
+ const width = STATE.render.options.width;
744
+ const height = STATE.render.options.height;
745
+
746
+ // Grid
747
+ context.beginPath();
748
+ context.strokeStyle = "rgba(0, 0, 0, 0.1)";
749
+ context.lineWidth = 1;
750
+ context.font = "bold 10px 'Fira Code'";
751
+ context.fillStyle = "rgba(0, 0, 0, 0.3)";
752
+ context.textAlign = "center";
753
+
754
+ for (let i = 0; i <= CONFIG.gridWidth; i++) {
755
+ const x = (width * i) / CONFIG.gridWidth;
756
+ context.moveTo(x, 0);
757
+ context.lineTo(x, height);
758
+ if (i > 0 && i < CONFIG.gridWidth) context.fillText(i, x, 15);
759
+ }
760
+ for (let i = 0; i <= CONFIG.gridHeight; i++) {
761
+ const y = (height * i) / CONFIG.gridHeight;
762
+ context.moveTo(0, y);
763
+ context.lineTo(width, y);
764
+ if (i > 0 && i < CONFIG.gridHeight) context.fillText(i, 15, y + 4);
765
+ }
766
+ context.stroke();
767
+
768
+ // Custom Objects
769
+ if (!STATE.isPlaying && !STATE.isFinished) {
770
+ context.font = "bold 14px 'Fira Code', monospace";
771
+ context.textAlign = "center";
772
+ context.textBaseline = "middle";
773
+
774
+ STATE.plannedActions.forEach((action) => {
775
+ if (action.previewBody) {
776
+ const body = action.previewBody;
777
+ const x = body.position.x;
778
+ const y = body.position.y;
779
+
780
+ if (action.delay > 0) {
781
+ context.fillStyle = "rgba(0, 0, 0, 0.7)";
782
+ context.fillText(`in ${action.delay}s`, x, body.bounds.max.y + 20);
783
+ }
784
+ if (action.params.velocity) {
785
+ const vx = action.params.velocity[0],
786
+ vy = action.params.velocity[1];
787
+ drawVelocityArrow(context, x, y, vx, vy, "#ef4444");
788
+ }
789
+ }
790
+ });
791
+
792
+ // Draw velocity arrows for any dynamic body in the world (Level setup items)
793
+ Matter.Composite.allBodies(STATE.engine.world).forEach((body) => {
794
+ // Skip planned action previews
795
+ if (STATE.plannedActions.some((a) => a.previewBody === body)) return;
796
+
797
+ const vx = body.velocity.x || 0;
798
+ const vy = body.velocity.y || 0;
799
+ if (Math.hypot(vx, vy) > 0.1) {
800
+ let color = body.render.fillStyle;
801
+ // Use red for dark objects (enemies), otherwise use body color
802
+ if (color === "#333" || color === CONFIG.colors.wall) color = "#ef4444";
803
+ drawVelocityArrow(context, body.position.x, body.position.y, vx, vy, color);
804
+ }
805
+ });
806
+ }
807
+
808
+ // Goal Zone
809
+ const bodies = Matter.Composite.allBodies(STATE.engine.world);
810
+ bodies.forEach((body) => {
811
+ if (body.label === "GoalZone") {
812
+ const bounds = body.bounds;
813
+ const w = bounds.max.x - bounds.min.x;
814
+ const h = bounds.max.y - bounds.min.y;
815
+ const cx = (bounds.min.x + bounds.max.x) / 2;
816
+ const cy = (bounds.min.y + bounds.max.y) / 2;
817
+
818
+ context.save();
819
+ context.translate(cx, cy);
820
+ context.beginPath();
821
+ context.rect(-w / 2, -h / 2, w, h);
822
+ context.strokeStyle = "#4ade80";
823
+ context.lineWidth = 3;
824
+ context.setLineDash([15, 10]);
825
+ context.stroke();
826
+
827
+ context.fillStyle = "#1a1a1a";
828
+ context.fillRect(-2, -20, 4, 40);
829
+ context.beginPath();
830
+ context.moveTo(2, -20);
831
+ context.lineTo(25, -10);
832
+ context.lineTo(2, 0);
833
+ context.fill();
834
+ context.font = "bold 14px 'Space Grotesk'";
835
+ context.textAlign = "center";
836
+ context.fillText("GOAL", 0, 35);
837
+ context.restore();
838
+ }
839
+ });
840
+ });
841
+
842
+ const canvas = STATE.render.canvas;
843
+ canvas.style.border = "3px solid black";
844
+
845
+ const resizeCanvas = () => {
846
+ const cw = container.clientWidth - 40;
847
+ const ch = container.clientHeight - 40;
848
+ const tr = CONFIG.width / CONFIG.height;
849
+ const cr = cw / ch;
850
+
851
+ if (cr > tr) {
852
+ canvas.style.height = `${ch}px`;
853
+ canvas.style.width = `${ch * tr}px`;
854
+ } else {
855
+ canvas.style.width = `${cw}px`;
856
+ canvas.style.height = `${cw / tr}px`;
857
+ }
858
+ };
859
+
860
+ const resizeObserver = new ResizeObserver(() => {
861
+ window.requestAnimationFrame(() => resizeCanvas());
862
+ });
863
+ resizeObserver.observe(container);
864
+ window.requestAnimationFrame(() => resizeCanvas());
865
+
866
+ STATE.runner = Matter.Runner.create({ isFixed: true, delta: 1000 / 60 });
867
+ Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
868
+ Matter.Render.run(STATE.render);
869
+ }
870
+
871
+ function drawVelocityArrow(context, x, y, vx, vy, color) {
872
+ if (vx === 0 && vy === 0) return;
873
+ const speed = Math.hypot(vx, vy);
874
+ const arrowScale = 20;
875
+ const maxLength = 150;
876
+ const rawLen = speed * arrowScale;
877
+ const scaleFactor = Math.min(rawLen, maxLength) / speed;
878
+ const endX = x + vx * scaleFactor;
879
+ const endY = y + vy * scaleFactor;
880
+
881
+ context.beginPath();
882
+ context.moveTo(x, y);
883
+ context.lineTo(endX, endY);
884
+ context.strokeStyle = color;
885
+ context.lineWidth = 3;
886
+ context.stroke();
887
+
888
+ const angle = Math.atan2(endY - y, endX - x);
889
+ const headLen = 12;
890
+ context.beginPath();
891
+ context.moveTo(endX, endY);
892
+ context.lineTo(endX - headLen * Math.cos(angle - Math.PI / 6), endY - headLen * Math.sin(angle - Math.PI / 6));
893
+ context.lineTo(endX - headLen * Math.cos(angle + Math.PI / 6), endY - headLen * Math.sin(angle + Math.PI / 6));
894
+ context.lineTo(endX, endY);
895
+ context.fillStyle = color;
896
+ context.fill();
897
+
898
+ context.fillStyle = color;
899
+ context.font = "bold 12px 'Fira Code', monospace";
900
+ context.textAlign = "center";
901
+ context.fillText(`${speed.toFixed(1)}`, endX + (vx / speed) * 15, endY + (vy / speed) * 15);
902
+ }
903
+
904
+ function handleUpdate() {
905
+ if (!STATE.isPlaying || STATE.isFinished) return;
906
+ STATE.time = performance.now() - STATE.startTime;
907
+ document.getElementById("timer-display").innerText = (STATE.time / 1000).toFixed(2) + "s";
908
+ STATE.eventQueue
909
+ .filter((e) => e.delay <= STATE.time && !e.executed)
910
+ .forEach((e) => {
911
+ e.action();
912
+ e.executed = true;
913
+ });
914
+ if (STATE.winCondition && STATE.winCondition()) endGame(true);
915
+ }
916
+
917
+ function loadLevel(index) {
918
+ if (STATE.runner) Matter.Runner.stop(STATE.runner);
919
+
920
+ document.getElementById("view-menu").classList.add("hidden");
921
+ document.getElementById("view-game").classList.remove("hidden");
922
+ document.getElementById("view-game").classList.add("flex");
923
+
924
+ STATE.currentLevelIndex = index;
925
+ const level = LEVELS[index];
926
+
927
+ const diffs = ["", "EASY", "MEDIUM", "HARD", "EXTREME"];
928
+ const diffColors = ["", "bg-neo-green", "bg-neo-yellow", "bg-neo-red", "bg-neo-purple"];
929
+ const d = level.difficulty || 1;
930
+ document.getElementById("level-indicator").innerHTML = `LEVEL ${index + 1} <span class="ml-2 px-1 ${diffColors[d]} text-black text-[10px] border border-black">${diffs[d]}</span>`;
931
+ document.getElementById("level-title").innerText = level.title;
932
+ document.getElementById("level-desc").innerText = level.desc;
933
+ document.getElementById("level-hint-text").innerText = level.hint;
934
+ document.getElementById("star-reqs").innerText = `3★ < ${level.stars[0] + 1} items`;
935
+ document.getElementById("hint-content").classList.add("hidden");
936
+
937
+ resetSimulationState();
938
+ document.getElementById("code-input").value = "";
939
+
940
+ const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, () => {});
941
+
942
+ STATE.currentBall = ball;
943
+ STATE.winCondition = () => Matter.Collision.collides(ball, goal) !== null;
944
+ Matter.Render.world(STATE.render);
945
+ }
946
+
947
+ function resetSimulationState() {
948
+ Matter.Composite.clear(STATE.engine.world);
949
+ STATE.engine.events = {};
950
+ Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
951
+
952
+ STATE.plannedActions = [];
953
+ STATE.eventQueue = [];
954
+ STATE.time = 0;
955
+ STATE.isFinished = false;
956
+ STATE.isPlaying = false;
957
+ STATE.currentBall = null;
958
+
959
+ document.getElementById("timer-display").innerText = "0.00s";
960
+ document.getElementById("win-message").style.display = "none";
961
+ document.getElementById("error-log").classList.add("hidden");
962
+ document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
963
+
964
+ updateActionList();
965
+ }
966
+
967
+ function endGame(success) {
968
+ STATE.isFinished = true;
969
+ STATE.isPlaying = false;
970
+ Matter.Runner.stop(STATE.runner);
971
+
972
+ if (success) {
973
+ document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black"></div> COMPLETE`;
974
+ const used = STATE.plannedActions.length;
975
+ const level = LEVELS[STATE.currentLevelIndex];
976
+ let stars = 1;
977
+ if (used <= level.stars[0]) stars = 3;
978
+ else if (used <= level.stars[1]) stars = 2;
979
+
980
+ if (stars > (STATE.progress.stars[level.id] || 0)) STATE.progress.stars[level.id] = stars;
981
+ if (STATE.currentLevelIndex >= STATE.progress.unlockedIndex && STATE.currentLevelIndex < LEVELS.length - 1) {
982
+ STATE.progress.unlockedIndex = STATE.currentLevelIndex + 1;
983
+ }
984
+ saveProgress();
985
+
986
+ const starContainer = document.getElementById("result-stars");
987
+ starContainer.innerHTML = "";
988
+ for (let i = 0; i < 3; i++) {
989
+ const filled = i < stars;
990
+ starContainer.innerHTML += `<i class="fas fa-star ${filled ? "text-yellow-400" : "text-gray-600"}"></i>`;
991
+ }
992
+ document.getElementById("result-items").innerText = used;
993
+
994
+ const btnNext = document.getElementById("btn-next-level");
995
+ if (STATE.currentLevelIndex >= LEVELS.length - 1) {
996
+ // Update Max
997
+ btnNext.style.display = "none";
998
+ } else {
999
+ btnNext.style.display = "inline-block";
1000
+ btnNext.onclick = () => loadLevel(STATE.currentLevelIndex + 1);
1001
+ }
1002
+ document.getElementById("win-message").style.display = "flex";
1003
+ }
1004
+ }
1005
+
1006
+ function updateActionList() {
1007
+ const list = document.getElementById("action-list");
1008
+ list.innerHTML = "";
1009
+ if (STATE.plannedActions.length === 0) {
1010
+ list.innerHTML = `<div class="text-xs text-gray-400 text-center mt-4 italic">No elements added yet.</div>`;
1011
+ return;
1012
+ }
1013
+ STATE.plannedActions.forEach((action, index) => {
1014
+ const div = document.createElement("div");
1015
+ div.className = "action-item flex justify-between items-center bg-white border border-black p-2 shadow-[2px_2px_0px_0px_rgba(0,0,0,0.1)] hover:shadow-[2px_2px_0px_0px_#000] transition-shadow group";
1016
+
1017
+ const p = action.params;
1018
+ const details = [];
1019
+ if (p.size !== 1) details.push(`size:${p.size}`);
1020
+ if (p.weight !== 1) details.push(`mass:${p.weight}`);
1021
+ if (p.restitution !== 0) details.push(`bounce:${p.restitution}`);
1022
+ if (p.angle !== 0) details.push(`angle:${p.angle}°`);
1023
+ if (p.velocity && (p.velocity[0] !== 0 || p.velocity[1] !== 0)) details.push(`v:[${p.velocity}]`);
1024
+ if (p.isStatic !== (p.shape === 'line')) details.push(p.isStatic ? 'static' : 'dynamic');
1025
+ if (p.delay > 0) details.push(`delay:${p.delay}s`);
1026
+
1027
+ const typeName = p.shape;
1028
+ const color = p.color || "#000";
1029
+ div.innerHTML = `
1030
+ <div class="flex items-center gap-2">
1031
+ <div class="w-3 h-3 border border-black" style="background-color: ${color}"></div>
1032
+ <div class="flex flex-col">
1033
+ <div class="flex items-center gap-2">
1034
+ <span class="text-xs font-bold uppercase">${typeName}</span>
1035
+ <span class="text-[10px] text-gray-500 font-mono">@ [${p.location[0]}, ${p.location[1]}]</span>
1036
+ </div>
1037
+ ${details.length > 0 ? `<div class="text-[9px] text-gray-400 font-mono leading-tight uppercase">${details.join(' | ')}</div>` : ''}
1038
+ </div>
1039
+ </div>
1040
+ `;
1041
+ const btnDelete = document.createElement("button");
1042
+ btnDelete.className = "btn-delete w-6 h-6 flex items-center justify-center bg-red-100 hover:bg-red-500 hover:text-white text-red-500 border border-black rounded transition-colors";
1043
+ btnDelete.innerHTML = `<i class="fas fa-times text-xs"></i>`;
1044
+ btnDelete.onclick = () => removeAction(index);
1045
+ div.appendChild(btnDelete);
1046
+ list.appendChild(div);
1047
+ });
1048
+ }
1049
+
1050
+ function removeAction(index) {
1051
+ const action = STATE.plannedActions[index];
1052
+ if (action.previewBody) Matter.World.remove(STATE.engine.world, action.previewBody);
1053
+ STATE.plannedActions.splice(index, 1);
1054
+ updateActionList();
1055
+ }
1056
+
1057
+ function createBody(shape, x, y, pixelSize, options) {
1058
+ switch (shape) {
1059
+ case "circle":
1060
+ case "ball":
1061
+ return Matter.Bodies.circle(x, y, pixelSize / 2, options);
1062
+ case "triangle":
1063
+ return Matter.Bodies.polygon(x, y, 3, pixelSize / 1.5, options);
1064
+ case "line":
1065
+ const thickness = 5;
1066
+ return Matter.Bodies.rectangle(x, y, pixelSize, thickness, options);
1067
+ default: // square, rectangle, etc.
1068
+ return Matter.Bodies.rectangle(x, y, pixelSize, pixelSize, options);
1069
+ }
1070
+ }
1071
+
1072
+ // --- UNIFIED TOOL IMPLEMENTATION ---
1073
+ const Tools = {
1074
+ add: (params) => {
1075
+ let {
1076
+ shape = "square",
1077
+ location = [CONFIG.gridWidth / 2, CONFIG.gridHeight / 2],
1078
+ size = 1,
1079
+ color = CONFIG.colors.userShape,
1080
+ mass = 1,
1081
+ weight = params.weight || mass,
1082
+ delay = 0,
1083
+ restitution = 0,
1084
+ friction = 0.1,
1085
+ rotation = 0,
1086
+ angle = params.angle || rotation,
1087
+ velocity = [0, 0],
1088
+ static: isStaticParam,
1089
+ isStatic = params.isStatic !== undefined ? params.isStatic : (isStaticParam !== undefined ? isStaticParam : ["platform", "line"].includes(shape)),
1090
+ } = params;
1091
+
1092
+ // Handle location as string (comma-separated or descriptive)
1093
+ if (typeof location === 'string') {
1094
+ const parts = location.match(/\d+/g);
1095
+ if (parts && parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
1096
+ location = [parseFloat(parts[0]), parseFloat(parts[1])];
1097
+ } else {
1098
+ const loc = location.toLowerCase();
1099
+ const midX = CONFIG.gridWidth / 2;
1100
+ const midY = CONFIG.gridHeight / 2;
1101
+ const margin = 2;
1102
+
1103
+ const locations = {
1104
+ "center": [midX, midY],
1105
+ "top-left": [margin, margin],
1106
+ "top-center": [midX, margin],
1107
+ "top-right": [CONFIG.gridWidth - margin, margin],
1108
+ "center-left": [margin, midY],
1109
+ "center": [midX, midY],
1110
+ "center-right": [CONFIG.gridWidth - margin, midY],
1111
+ "bottom-left": [margin, CONFIG.gridHeight - margin],
1112
+ "bottom-center": [midX, CONFIG.gridHeight - margin],
1113
+ "bottom-right": [CONFIG.gridWidth - margin, CONFIG.gridHeight - margin],
1114
+ };
1115
+ location = locations[loc] || [midX, midY];
1116
+ }
1117
+ }
1118
+
1119
+ // Ensure location is an array
1120
+ if (!Array.isArray(location) || location.length !== 2) {
1121
+ console.warn("Invalid location format. Defaulting to center.");
1122
+ location = [CONFIG.gridWidth / 2, CONFIG.gridHeight / 2];
1123
+ }
1124
+
1125
+ const finalParams = {
1126
+ shape,
1127
+ location,
1128
+ size,
1129
+ color,
1130
+ weight,
1131
+ delay,
1132
+ restitution,
1133
+ friction,
1134
+ angle,
1135
+ velocity,
1136
+ isStatic,
1137
+ };
1138
+
1139
+ const x = pX(location[0]);
1140
+ const y = pY(location[1]);
1141
+ const pixelSize = pX(size); // Use grid-relative sizing
1142
+
1143
+ const commonProps = {
1144
+ angle: (angle * Math.PI) / 180,
1145
+ restitution: restitution,
1146
+ friction: friction,
1147
+ render: { fillStyle: color, ...strokeStyle },
1148
+ density: weight * 0.005,
1149
+ };
1150
+
1151
+ // For preview (ghosts)
1152
+ const previewProps = {
1153
+ ...commonProps,
1154
+ isStatic: true,
1155
+ isSensor: true,
1156
+ render: {
1157
+ fillStyle: color,
1158
+ opacity: 0.4,
1159
+ strokeStyle: "#000000",
1160
+ lineWidth: 2,
1161
+ },
1162
+ };
1163
+
1164
+ const body = createBody(shape, x, y, pixelSize, previewProps);
1165
+
1166
+ Matter.World.add(STATE.engine.world, body);
1167
+
1168
+ STATE.plannedActions.push({
1169
+ type: "add",
1170
+ params: finalParams, // Use the complete object
1171
+ delay: delay || 0,
1172
+ previewBody: body,
1173
+ });
1174
+ },
1175
+ };
1176
+
1177
+ // --- AI Model Setup ---
1178
+ const MODEL_ID = "Xenova/functiongemma-270m-game";
1179
+ let tokenizer, model;
1180
+
1181
+ const TOOL_SCHEMA = [{"type": "function", "function": {"name": "add", "description": "Add a shape into the game scene.", "parameters": {"type": "object", "properties": {"shape": {"type": "string", "enum": ["circle", "square", "triangle", "star", "rectangle", "line", "ellipse"], "description": "The kind shape to add. Required."}, "location": {"type": "string", "description": "The [x, y] coordinates where the shape will be placed or a descriptive string. Required."}, "size": {"type": "number", "description": "The size of the object (between 0.1 and 10.0). Default is 1.0."}, "rotation": {"type": "integer", "description": "The initial clockwise rotation of the object in degrees (0-360). Default is 0."}, "friction": {"type": "number", "description": "The friction of the object (between 0.0 and 1.0). Default is 0.0."}, "restitution": {"type": "number", "description": "The bounciness of the object (between 0.0 and 1.0). Default is 0.0."}, "mass": {"type": "number", "description": "The mass of the object (between 1.0 and 10.0). Default is 1.0."}, "delay": {"type": "number", "description": "The time in seconds to wait before the object appears in the scene. Default is 0.0."}, "static": {"type": "boolean", "description": "Whether the object is static (immovable) or dynamic. Default is False."}, "velocity": {"type": "array", "items": {"type": "number"}, "description": "The initial [vx, vy] velocity vector of the object (values between -10.0 and 10.0). Default is [0.0, 0.0]."}, "color": {"type": "string", "description": "The color of the object as a string or hex code (e.g., \"red\", \"blue\", \"#FF00FF\"). Default is \"red\"."}}, "required": ["shape", "location"]}, "return": {"type": "string", "description": "A unique identifier for the added shape."}}}];
1182
+
1183
+ async function initModel() {
1184
+ try {
1185
+ tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
1186
+ model = await AutoModelForCausalLM.from_pretrained(MODEL_ID, {
1187
+ device: "webgpu",
1188
+ dtype: "q4",
1189
+ });
1190
+ document.getElementById("loading-overlay").classList.add("hidden");
1191
+ } catch (e) {
1192
+ console.error(e);
1193
+ document.getElementById("loading-overlay").innerHTML = `<div class="text-red-500 font-bold p-4 bg-white border-2 border-black">Error: ${e.message}</div>`;
1194
+ }
1195
+ }
1196
+
1197
+ // --- Command Execution ---
1198
+ async function executeCommand() {
1199
+ const input = document.getElementById("code-input").value.trim();
1200
+ if (!input) return;
1201
+
1202
+ const btn = document.getElementById("btn-execute");
1203
+ const errorLog = document.getElementById("error-log");
1204
+
1205
+ // UI Loading State
1206
+ btn.disabled = true;
1207
+ btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> THINKING...`;
1208
+ errorLog.classList.add("hidden");
1209
+
1210
+ const systemPrompt = `You are a model that can do function calling with the following functions`;
1211
+
1212
+ try {
1213
+ const lines = input.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith("//"));
1214
+
1215
+ for (const line of lines) {
1216
+ // 2. Prepare Messages
1217
+ const messages = [
1218
+ { role: "developer", content: systemPrompt },
1219
+ { role: "user", content: line },
1220
+ ];
1221
+
1222
+ // 3. Apply Template
1223
+ const inputs = tokenizer.apply_chat_template(messages, {
1224
+ tools: TOOL_SCHEMA,
1225
+ tokenize: true,
1226
+ add_generation_prompt: true,
1227
+ return_dict: true,
1228
+ });
1229
+
1230
+ // 4. Generate
1231
+ const output = await model.generate({ ...inputs, max_new_tokens: 128, do_sample: false });
1232
+ const decoded = tokenizer.decode(output.slice(0, [inputs.input_ids.dims[1], null]), { skip_special_tokens: false });
1233
+
1234
+ // 5. Parse Output
1235
+ // Format: <start_function_call>call:add{...}<end_function_call>
1236
+ const startTag = "<start_function_call>";
1237
+ const endTag = "<end_function_call>";
1238
+ const startIndex = decoded.indexOf(startTag);
1239
+ const endIndex = decoded.indexOf(endTag);
1240
+
1241
+ if (startIndex !== -1 && endIndex !== -1) {
1242
+ let callStr = decoded.substring(startIndex + startTag.length, endIndex);
1243
+ if (callStr.startsWith("call:add")) {
1244
+ // Extract JSON-like string: {location:[...],shape:<escape>...<escape>}
1245
+ let argsStr = callStr.substring(callStr.indexOf("{"));
1246
+
1247
+ // Sanitize to valid JSON
1248
+ argsStr = argsStr
1249
+ .replace(/<escape>(.*?)<escape>/g, '"$1"') // Handle string escapes
1250
+ .replace(/(\w+):/g, '"$1":'); // Quote keys
1251
+
1252
+ const args = JSON.parse(argsStr);
1253
+ Tools.add(args);
1254
+ } else {
1255
+ throw new Error("Model did not generate a valid add command.");
1256
+ }
1257
+ } else {
1258
+ throw new Error(`Could not understand command: "${line}"`);
1259
+ }
1260
+ }
1261
+ document.getElementById("code-input").value = "";
1262
+ } catch (err) {
1263
+ errorLog.innerText = err.message;
1264
+ errorLog.classList.remove("hidden");
1265
+ console.error(err);
1266
+ } finally {
1267
+ btn.disabled = false;
1268
+ btn.innerHTML = `<i class="fas fa-terminal"></i> EXECUTE`;
1269
+ updateActionList();
1270
+ }
1271
+ }
1272
+
1273
+ // --- Real Simulation Run ---
1274
+ function runSimulation() {
1275
+ if (STATE.runner) Matter.Runner.stop(STATE.runner);
1276
+
1277
+ const storedActions = [...STATE.plannedActions];
1278
+ const level = LEVELS[STATE.currentLevelIndex];
1279
+
1280
+ Matter.Composite.clear(STATE.engine.world);
1281
+ STATE.engine.events = {};
1282
+ Matter.Events.on(STATE.engine, "beforeUpdate", handleUpdate);
1283
+ STATE.eventQueue = [];
1284
+ STATE.isFinished = false;
1285
+
1286
+ const addEvent = (delay, action) => {
1287
+ STATE.eventQueue.push({
1288
+ delay: delay * 1000,
1289
+ action: action,
1290
+ executed: false,
1291
+ });
1292
+ };
1293
+
1294
+ const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, addEvent);
1295
+
1296
+ STATE.currentBall = ball;
1297
+ STATE.winCondition = () => Matter.Collision.collides(ball, goal) !== null;
1298
+
1299
+ // Real Action Executioner
1300
+ const executeReal = (params) => {
1301
+ const { shape, location, size, weight, restitution, friction, angle, velocity, isStatic } = params;
1302
+ const x = pX(location[0]),
1303
+ y = pY(location[1]);
1304
+ const pixelSize = pX(size);
1305
+
1306
+ const props = {
1307
+ angle: (angle * Math.PI) / 180,
1308
+ restitution,
1309
+ friction,
1310
+ isStatic,
1311
+ render: strokeStyle,
1312
+ density: weight * 0.005,
1313
+ };
1314
+
1315
+ const body = createBody(shape, x, y, pixelSize, props);
1316
+
1317
+ if (!isStatic && (velocity[0] !== 0 || velocity[1] !== 0)) {
1318
+ Matter.Body.setVelocity(body, { x: velocity[0], y: velocity[1] });
1319
+ }
1320
+
1321
+ Matter.World.add(STATE.engine.world, body);
1322
+ };
1323
+
1324
+ storedActions.forEach((action) => {
1325
+ STATE.eventQueue.push({
1326
+ delay: action.delay * 1000,
1327
+ action: () => executeReal(action.params),
1328
+ executed: false,
1329
+ });
1330
+ });
1331
+
1332
+ STATE.plannedActions = storedActions;
1333
+
1334
+ STATE.isPlaying = true;
1335
+ STATE.startTime = performance.now();
1336
+ document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-green-500 rounded-full border border-black animate-ping"></div> RUNNING`;
1337
+ Matter.Runner.run(STATE.runner, STATE.engine);
1338
+ }
1339
+
1340
+ function renderMenu() {
1341
+ if (STATE.runner) Matter.Runner.stop(STATE.runner);
1342
+
1343
+ document.getElementById("view-game").classList.add("hidden");
1344
+ document.getElementById("view-game").classList.remove("flex");
1345
+ document.getElementById("view-menu").classList.remove("hidden");
1346
+
1347
+ const grid = document.getElementById("level-grid");
1348
+ grid.innerHTML = "";
1349
+
1350
+ LEVELS.forEach((level, index) => {
1351
+ const unlocked = index <= STATE.progress.unlockedIndex;
1352
+ const stars = STATE.progress.stars[index] || 0;
1353
+
1354
+ const card = document.createElement("div");
1355
+ card.className = `level-card neo-border p-4 flex flex-col items-center justify-center gap-2 aspect-square ${unlocked ? "bg-white cursor-pointer" : "locked"}`;
1356
+
1357
+ if (unlocked) {
1358
+ card.onclick = () => loadLevel(index);
1359
+ card.innerHTML = `
1360
+ <div class="text-3xl font-black">${index + 1}</div>
1361
+ <div class="flex gap-1 text-xs">
1362
+ ${Array(3)
1363
+ .fill(0)
1364
+ .map((_, i) => `<i class="fas fa-star ${i < stars ? "text-neo-yellow" : "text-gray-300"}"></i>`)
1365
+ .join("")}
1366
+ </div>
1367
+ `;
1368
+ } else {
1369
+ card.innerHTML = `<i class="fas fa-lock text-gray-400 text-2xl"></i>`;
1370
+ }
1371
+ grid.appendChild(card);
1372
+ });
1373
+ }
1374
+
1375
+ // --- Bindings ---
1376
+ document.getElementById("btn-execute").onclick = executeCommand;
1377
+ document.getElementById("btn-play").onclick = runSimulation;
1378
+
1379
+ document.getElementById("btn-reset").onclick = () => {
1380
+ Matter.Runner.stop(STATE.runner);
1381
+ const level = LEVELS[STATE.currentLevelIndex];
1382
+ Matter.Composite.clear(STATE.engine.world);
1383
+ const { ball, goal } = level.setup(Matter.World, Matter.Bodies, STATE.engine.world, () => {});
1384
+
1385
+ STATE.currentBall = ball;
1386
+
1387
+ STATE.plannedActions.forEach((action) => {
1388
+ Matter.World.add(STATE.engine.world, action.previewBody);
1389
+ });
1390
+
1391
+ STATE.isPlaying = false;
1392
+ document.getElementById("status-badge").innerHTML = `<div class="w-3 h-3 bg-red-500 rounded-full border border-black animate-pulse"></div> READY`;
1393
+ document.getElementById("timer-display").innerText = "0.00s";
1394
+ document.getElementById("win-message").style.display = "none";
1395
+ };
1396
+
1397
+ document.getElementById("btn-clear-all").onclick = () => {
1398
+ document.getElementById("code-input").value = "";
1399
+ STATE.plannedActions.forEach((action) => {
1400
+ if (action.previewBody) Matter.World.remove(STATE.engine.world, action.previewBody);
1401
+ });
1402
+ STATE.plannedActions = [];
1403
+ updateActionList();
1404
+ document.getElementById("btn-reset").click();
1405
+ };
1406
+
1407
+ document.getElementById("btn-back").onclick = renderMenu;
1408
+ document.getElementById("btn-hint").onclick = () => {
1409
+ const content = document.getElementById("hint-content");
1410
+ content.classList.toggle("hidden");
1411
+ };
1412
+
1413
+ document.getElementById("btn-solution").addEventListener("click", () => {
1414
+ const level = LEVELS[STATE.currentLevelIndex];
1415
+ if (level.solution) {
1416
+ document.getElementById("code-input").value = level.solution;
1417
+ }
1418
+ });
1419
+
1420
+ window.onload = () => {
1421
+ loadProgress();
1422
+ initPhysics();
1423
+ renderMenu();
1424
+ initModel();
1425
+ };
1426
+ </script>
1427
+ </body>
1428
+
1429
+ </html>