ale-bena commited on
Commit
b9b3738
·
verified ·
1 Parent(s): 2000c07

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1674 -19
index.html CHANGED
@@ -1,19 +1,1674 @@
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>Map Builder</title>
8
+ <link rel="icon" type="image/png" href="logo.png?v=2">
9
+ <!-- Google Fonts -->
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link
13
+ href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
14
+ rel="stylesheet">
15
+
16
+ <style>
17
+ /* --- CSS Reset & Variables --- */
18
+ :root {
19
+ --font-heading: 'Outfit', sans-serif;
20
+ --font-body: 'Plus Jakarta Sans', sans-serif;
21
+ --font-mono: 'JetBrains Mono', monospace;
22
+
23
+ /* Dark Theme Colors (Default) */
24
+ --bg-page: #0b0f19;
25
+ --bg-card: rgba(17, 24, 39, 0.75);
26
+ --bg-sidebar: rgba(10, 14, 23, 0.9);
27
+ --border-color: rgba(255, 255, 255, 0.08);
28
+ --text-main: #f3f4f6;
29
+ --text-muted: #9ca3af;
30
+ --text-inverse: #0b0f19;
31
+
32
+ --primary: #6366f1;
33
+ --primary-glow: rgba(99, 102, 241, 0.15);
34
+ --secondary: #a855f7;
35
+ --accent-cyan: #06b6d4;
36
+ --accent-green: #10b981;
37
+ --accent-orange: #f97316;
38
+ --accent-red: #ef4444;
39
+
40
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
41
+ --shadow-md: 0 8px 30px rgba(0, 0, 0, 0.3);
42
+ --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.2);
43
+
44
+ --transition-fast: 0.2s ease;
45
+ --transition-normal: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
46
+
47
+ --glass-bg: rgba(255, 255, 255, 0.03);
48
+ --glass-blur: blur(12px);
49
+ }
50
+
51
+ * {
52
+ margin: 0;
53
+ padding: 0;
54
+ box-sizing: border-box;
55
+ }
56
+
57
+ html {
58
+ scroll-behavior: smooth;
59
+ }
60
+
61
+ body {
62
+ background-color: var(--bg-page);
63
+ color: var(--text-main);
64
+ font-family: var(--font-body);
65
+ line-height: 1.6;
66
+ overflow-x: hidden;
67
+ transition: background-color var(--transition-normal), color var(--transition-normal);
68
+ position: relative;
69
+ min-height: 100vh;
70
+ }
71
+
72
+ /* --- Custom Scrollbar --- */
73
+ ::-webkit-scrollbar {
74
+ width: 8px;
75
+ height: 8px;
76
+ }
77
+
78
+ ::-webkit-scrollbar-track {
79
+ background: var(--bg-page);
80
+ }
81
+
82
+ ::-webkit-scrollbar-thumb {
83
+ background: var(--border-color);
84
+ border-radius: 4px;
85
+ }
86
+
87
+ ::-webkit-scrollbar-thumb:hover {
88
+ background: var(--primary);
89
+ }
90
+
91
+ /* --- Dynamic Animated Background Glows --- */
92
+ .bg-glows {
93
+ position: fixed;
94
+ top: 0;
95
+ left: 0;
96
+ width: 100vw;
97
+ height: 100vh;
98
+ pointer-events: none;
99
+ z-index: 0;
100
+ overflow: hidden;
101
+ }
102
+
103
+ .glow-1 {
104
+ position: absolute;
105
+ top: -10%;
106
+ right: 5%;
107
+ width: 50vw;
108
+ height: 50vw;
109
+ background: radial-gradient(circle, rgba(99, 102, 241, 0.08) 0%, transparent 70%);
110
+ filter: blur(80px);
111
+ animation: floatGlow 15s infinite alternate;
112
+ }
113
+
114
+ .glow-2 {
115
+ position: absolute;
116
+ bottom: -10%;
117
+ left: 10%;
118
+ width: 45vw;
119
+ height: 45vw;
120
+ background: radial-gradient(circle, rgba(168, 85, 247, 0.06) 0%, transparent 70%);
121
+ filter: blur(80px);
122
+ animation: floatGlow 20s infinite alternate-reverse;
123
+ }
124
+
125
+ @keyframes floatGlow {
126
+ 0% {
127
+ transform: translate(0, 0) scale(1);
128
+ }
129
+
130
+ 100% {
131
+ transform: translate(40px, -30px) scale(1.1);
132
+ }
133
+ }
134
+
135
+ /* --- Container --- */
136
+ .app-container {
137
+ max-width: 1400px;
138
+ margin: 0 auto;
139
+ padding: 2rem;
140
+ position: relative;
141
+ z-index: 1;
142
+ }
143
+
144
+ /* --- Header --- */
145
+ header {
146
+ margin-bottom: 2.5rem;
147
+ border-bottom: 1px solid var(--border-color);
148
+ padding-bottom: 1.5rem;
149
+ position: relative;
150
+ text-align: center;
151
+ display: flex;
152
+ flex-direction: column;
153
+ align-items: center;
154
+ }
155
+
156
+ .badge-header {
157
+ display: inline-flex;
158
+ background: linear-gradient(90deg, var(--primary-glow), transparent);
159
+ border-left: 3px solid var(--primary);
160
+ padding: 0.35rem 0.75rem;
161
+ font-size: 0.75rem;
162
+ font-weight: 700;
163
+ letter-spacing: 1px;
164
+ text-transform: uppercase;
165
+ color: var(--primary);
166
+ margin-bottom: 0.75rem;
167
+ border-radius: 0 4px 4px 0;
168
+ }
169
+
170
+ h1 {
171
+ font-family: var(--font-heading);
172
+ font-size: 2.5rem;
173
+ font-weight: 800;
174
+ background: linear-gradient(135deg, var(--text-main) 30%, var(--primary) 70%, var(--secondary) 100%);
175
+ -webkit-background-clip: text;
176
+ background-clip: text;
177
+ -webkit-text-fill-color: transparent;
178
+ letter-spacing: -1px;
179
+ margin-bottom: 0.5rem;
180
+ text-align: center;
181
+ }
182
+
183
+ .subtitle {
184
+ font-size: 1.05rem;
185
+ color: var(--text-muted);
186
+ font-weight: 400;
187
+ text-align: center;
188
+ }
189
+
190
+ /* --- Toast Notifications --- */
191
+ .toast-container {
192
+ position: fixed;
193
+ top: 2rem;
194
+ right: 2rem;
195
+ z-index: 9999;
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: 0.75rem;
199
+ pointer-events: none;
200
+ }
201
+
202
+ .toast {
203
+ pointer-events: auto;
204
+ background: rgba(15, 23, 42, 0.9);
205
+ backdrop-filter: blur(16px) saturate(180%);
206
+ -webkit-backdrop-filter: blur(16px) saturate(180%);
207
+ border: 1px solid var(--border-color);
208
+ color: var(--text-main);
209
+ padding: 1rem 1.5rem;
210
+ border-radius: 12px;
211
+ font-size: 0.9rem;
212
+ font-weight: 600;
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 0.75rem;
216
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.4), 0 8px 10px -6px rgba(0, 0, 0, 0.4);
217
+ transform: translateY(-20px);
218
+ opacity: 0;
219
+ transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.35s ease;
220
+ max-width: 380px;
221
+ }
222
+
223
+ .toast.show {
224
+ transform: translateY(0);
225
+ opacity: 1;
226
+ }
227
+
228
+ .toast.success {
229
+ border-left: 4px solid var(--accent-green);
230
+ }
231
+
232
+ .toast.error {
233
+ border-left: 4px solid var(--accent-red);
234
+ }
235
+
236
+ .toast-icon {
237
+ font-size: 1.25rem;
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ .toast-message {
242
+ flex-grow: 1;
243
+ }
244
+
245
+ /* --- Layout: Dynamic Wrap --- */
246
+ .tool-layout {
247
+ display: flex;
248
+ flex-wrap: wrap;
249
+ gap: 2rem;
250
+ width: 100%;
251
+ }
252
+
253
+ .editor-side {
254
+ flex: 2 1 600px;
255
+ display: flex;
256
+ flex-direction: column;
257
+ gap: 1.5rem;
258
+ min-width: 0;
259
+ }
260
+
261
+ .settings-side {
262
+ flex: 1 1 400px;
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 1.5rem;
266
+ min-width: 0;
267
+ }
268
+
269
+ /* --- Card Styling --- */
270
+ .card {
271
+ background-color: var(--bg-card);
272
+ border: 1px solid var(--border-color);
273
+ border-radius: 16px;
274
+ padding: 1.5rem;
275
+ backdrop-filter: var(--glass-blur);
276
+ box-shadow: var(--shadow-md);
277
+ transition: border-color var(--transition-fast);
278
+ }
279
+
280
+ .card:hover {
281
+ border-color: rgba(99, 102, 241, 0.25);
282
+ }
283
+
284
+ .card-title {
285
+ font-family: var(--font-heading);
286
+ font-weight: 700;
287
+ font-size: 1.2rem;
288
+ margin-bottom: 1.25rem;
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 0.5rem;
292
+ border-bottom: 1px solid var(--border-color);
293
+ padding-bottom: 0.75rem;
294
+ }
295
+
296
+ /* --- Painter Toolbar --- */
297
+ .toolbar {
298
+ display: flex;
299
+ flex-wrap: wrap;
300
+ gap: 0.5rem;
301
+ padding: 0.75rem;
302
+ background: rgba(255, 255, 255, 0.02);
303
+ border: 1px solid var(--border-color);
304
+ border-radius: 12px;
305
+ align-items: center;
306
+ }
307
+
308
+ .tool-btn {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 0.4rem;
312
+ padding: 0.5rem 0.75rem;
313
+ border-radius: 8px;
314
+ border: 1px solid var(--border-color);
315
+ background: transparent;
316
+ color: var(--text-main);
317
+ font-size: 0.85rem;
318
+ font-weight: 500;
319
+ cursor: pointer;
320
+ transition: all var(--transition-fast);
321
+ }
322
+
323
+ .tool-btn:hover:not(:disabled) {
324
+ background: rgba(255, 255, 255, 0.05);
325
+ border-color: var(--text-muted);
326
+ }
327
+
328
+ .tool-btn.active {
329
+ background: var(--primary);
330
+ border-color: var(--primary);
331
+ box-shadow: 0 0 10px var(--primary-glow);
332
+ }
333
+
334
+ .tool-btn:disabled {
335
+ opacity: 0.35;
336
+ cursor: not-allowed;
337
+ }
338
+
339
+ /* --- Grid Canvas Card --- */
340
+ .grid-container-card {
341
+ background: rgba(0, 0, 0, 0.35);
342
+ border: 1px solid var(--border-color);
343
+ border-radius: 16px;
344
+ display: flex;
345
+ flex-direction: column;
346
+ height: 550px;
347
+ min-height: 250px;
348
+ min-width: 300px;
349
+ max-width: 100%;
350
+ position: relative;
351
+ z-index: 2;
352
+ resize: both;
353
+ overflow: hidden;
354
+ box-shadow: var(--shadow-md);
355
+ }
356
+
357
+ .grid-wrapper {
358
+ flex: 1;
359
+ min-height: 0;
360
+ overflow: auto;
361
+ display: flex;
362
+ padding: 1rem;
363
+ background-image: radial-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 0);
364
+ background-size: 16px 16px;
365
+ }
366
+
367
+ .grid-canvas {
368
+ --cell-size: 32px;
369
+ display: grid;
370
+ gap: 1px;
371
+ background-color: var(--border-color);
372
+ border: 2px solid var(--border-color);
373
+ border-radius: 6px;
374
+ user-select: none;
375
+ margin: auto;
376
+ width: max-content;
377
+ height: max-content;
378
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
379
+ }
380
+
381
+ .grid-cell {
382
+ width: var(--cell-size);
383
+ height: var(--cell-size);
384
+ background-color: #080a14;
385
+ display: flex;
386
+ justify-content: center;
387
+ align-items: center;
388
+ font-size: calc(var(--cell-size) * 0.35);
389
+ font-weight: 700;
390
+ cursor: pointer;
391
+ transition: background-color var(--transition-fast), border var(--transition-fast);
392
+ border: 1px solid transparent;
393
+ }
394
+
395
+ .grid-cell:hover {
396
+ filter: brightness(1.25);
397
+ border-color: var(--primary);
398
+ }
399
+
400
+ /* --- Tile Palette Colors (identical to collab-agent) --- */
401
+ .grid-cell.tile-empty {
402
+ background-color: #080a14;
403
+ color: rgba(255, 255, 255, 0.15);
404
+ }
405
+
406
+ .grid-cell.tile-normal {
407
+ background-color: #1e293b;
408
+ color: #94a3b8;
409
+ }
410
+
411
+ .grid-cell.tile-spawn {
412
+ background-color: #059669;
413
+ color: #ffffff;
414
+ }
415
+
416
+ .grid-cell.tile-deliver {
417
+ background-color: #dc2626;
418
+ color: #ffffff;
419
+ }
420
+
421
+ .grid-cell.tile-crate {
422
+ background-color: #ca8a04;
423
+ color: #0f172a;
424
+ }
425
+
426
+ .grid-cell.tile-crate_spawn {
427
+ background-color: #ea580c;
428
+ color: #ffffff;
429
+ }
430
+
431
+ .grid-cell.tile-arrow {
432
+ background-color: #1e40af;
433
+ color: #ffffff;
434
+ font-size: calc(var(--cell-size) * 0.5);
435
+ }
436
+
437
+ /* --- Coordinate Tracker HUD --- */
438
+ .coord-overlay {
439
+ display: flex;
440
+ justify-content: space-between;
441
+ background: rgba(255, 255, 255, 0.02);
442
+ border: 1px solid var(--border-color);
443
+ border-radius: 10px;
444
+ padding: 0.75rem 1rem;
445
+ font-size: 0.85rem;
446
+ backdrop-filter: var(--glass-blur);
447
+ }
448
+
449
+ .coord-item span {
450
+ font-family: var(--font-mono);
451
+ font-weight: 700;
452
+ color: var(--accent-cyan);
453
+ }
454
+
455
+ /* --- Dynamic Form Groups --- */
456
+ .form-row {
457
+ display: flex;
458
+ gap: 1rem;
459
+ margin-bottom: 1rem;
460
+ }
461
+
462
+ .form-row .form-group {
463
+ flex: 1;
464
+ margin-bottom: 0;
465
+ }
466
+
467
+ .form-group {
468
+ margin-bottom: 1.25rem;
469
+ }
470
+
471
+ label {
472
+ display: block;
473
+ font-size: 0.75rem;
474
+ color: var(--text-muted);
475
+ font-weight: 600;
476
+ text-transform: uppercase;
477
+ margin-bottom: 0.4rem;
478
+ letter-spacing: 0.05em;
479
+ }
480
+
481
+ .form-input {
482
+ width: 100%;
483
+ background: rgba(255, 255, 255, 0.03);
484
+ border: 1px solid var(--border-color);
485
+ border-radius: 8px;
486
+ padding: 0.6rem 0.75rem;
487
+ color: var(--text-main);
488
+ font-family: inherit;
489
+ font-size: 0.9rem;
490
+ outline: none;
491
+ transition: all var(--transition-fast);
492
+ }
493
+
494
+ .form-input:focus {
495
+ border-color: var(--primary);
496
+ box-shadow: 0 0 8px var(--primary-glow);
497
+ }
498
+
499
+ .form-select {
500
+ cursor: pointer;
501
+ appearance: none;
502
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");
503
+ background-position: right 0.5rem center;
504
+ background-repeat: no-repeat;
505
+ background-size: 1.25rem;
506
+ padding-right: 2rem;
507
+ }
508
+
509
+ /* --- Buttons --- */
510
+ .btn {
511
+ display: inline-flex;
512
+ justify-content: center;
513
+ align-items: center;
514
+ gap: 0.5rem;
515
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
516
+ color: white;
517
+ border: none;
518
+ border-radius: 8px;
519
+ padding: 0.6rem 1rem;
520
+ font-weight: 600;
521
+ font-size: 0.88rem;
522
+ cursor: pointer;
523
+ transition: all var(--transition-fast);
524
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.15);
525
+ }
526
+
527
+ .btn:hover {
528
+ opacity: 0.95;
529
+ transform: translateY(-1px);
530
+ box-shadow: 0 4px 15px rgba(99, 102, 241, 0.25);
531
+ }
532
+
533
+ .btn-outline {
534
+ background: transparent !important;
535
+ border: 1px solid var(--border-color);
536
+ color: var(--text-main);
537
+ box-shadow: none;
538
+ }
539
+
540
+ .btn-outline:hover {
541
+ background: rgba(255, 255, 255, 0.04) !important;
542
+ border-color: var(--text-muted);
543
+ }
544
+
545
+ .btn-danger {
546
+ background: linear-gradient(135deg, var(--accent-red), #b91c1c) !important;
547
+ box-shadow: 0 4px 12px rgba(239, 68, 68, 0.15);
548
+ }
549
+
550
+ .btn-danger:hover {
551
+ box-shadow: 0 4px 15px rgba(239, 68, 68, 0.25);
552
+ }
553
+
554
+ /* --- JSON Output Textarea --- */
555
+ .json-textarea-container {
556
+ position: relative;
557
+ width: 100%;
558
+ }
559
+
560
+ .json-textarea {
561
+ width: 100%;
562
+ height: 350px;
563
+ background: rgba(0, 0, 0, 0.4);
564
+ border: 1px solid var(--border-color);
565
+ border-radius: 12px;
566
+ padding: 1rem;
567
+ font-family: var(--font-mono);
568
+ font-size: 0.85rem;
569
+ color: #818cf8;
570
+ outline: none;
571
+ resize: vertical;
572
+ line-height: 1.5;
573
+ transition: all var(--transition-fast);
574
+ }
575
+
576
+ .json-textarea:focus {
577
+ border-color: var(--primary);
578
+ background: rgba(0, 0, 0, 0.5);
579
+ box-shadow: 0 0 10px rgba(99, 102, 241, 0.1);
580
+ }
581
+
582
+ /* --- Validation Status Badges --- */
583
+ .validation-status {
584
+ display: flex;
585
+ align-items: center;
586
+ gap: 0.5rem;
587
+ padding: 0.5rem 0.75rem;
588
+ border-radius: 8px;
589
+ font-size: 0.8rem;
590
+ font-weight: 600;
591
+ margin-bottom: 0.75rem;
592
+ border: 1px solid transparent;
593
+ }
594
+
595
+ .validation-valid {
596
+ background-color: rgba(16, 185, 129, 0.1);
597
+ color: var(--accent-green);
598
+ border-color: rgba(16, 185, 129, 0.2);
599
+ }
600
+
601
+ .validation-invalid {
602
+ background-color: rgba(239, 68, 68, 0.1);
603
+ color: var(--accent-red);
604
+ border-color: rgba(239, 68, 68, 0.2);
605
+ font-family: var(--font-mono);
606
+ font-size: 0.75rem;
607
+ white-space: pre-wrap;
608
+ }
609
+
610
+ /* --- Dynamic NPC list --- */
611
+ .npc-list-container {
612
+ display: flex;
613
+ flex-direction: column;
614
+ gap: 0.5rem;
615
+ margin-bottom: 1rem;
616
+ }
617
+
618
+ .npc-row {
619
+ display: flex;
620
+ gap: 0.5rem;
621
+ align-items: center;
622
+ }
623
+
624
+ .npc-row>* {
625
+ flex-shrink: 1;
626
+ min-width: 0;
627
+ }
628
+
629
+ /* --- Keybind Helper Box --- */
630
+ .shortcuts-box {
631
+ display: grid;
632
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
633
+ gap: 0.75rem;
634
+ padding: 1rem;
635
+ background: rgba(255, 255, 255, 0.01);
636
+ border: 1px solid var(--border-color);
637
+ border-radius: 12px;
638
+ font-size: 0.78rem;
639
+ }
640
+
641
+ .shortcut-item {
642
+ display: flex;
643
+ align-items: center;
644
+ gap: 0.4rem;
645
+ color: var(--text-muted);
646
+ }
647
+
648
+ .shortcut-key {
649
+ font-family: var(--font-mono);
650
+ background: rgba(255, 255, 255, 0.08);
651
+ border: 1px solid rgba(255, 255, 255, 0.1);
652
+ color: var(--text-main);
653
+ padding: 0.1rem 0.35rem;
654
+ border-radius: 4px;
655
+ font-weight: 700;
656
+ }
657
+ </style>
658
+ </head>
659
+
660
+ <body>
661
+
662
+ <!-- Dynamic glows background -->
663
+ <div class="bg-glows">
664
+ <div class="glow-1"></div>
665
+ <div class="glow-2"></div>
666
+ </div>
667
+
668
+ <div class="app-container">
669
+
670
+ <header>
671
+ <h1>Map Builder</h1>
672
+ <div class="subtitle">Visually paint map grids, configure NPCS, and directly edit/validate the game
673
+ configuration JSON.</div>
674
+ </header>
675
+
676
+ <div class="tool-layout">
677
+
678
+ <!-- Left Column: Visual Editor -->
679
+ <div class="editor-side">
680
+
681
+ <!-- Dimension sizing & cell zoom controls -->
682
+ <div class="card">
683
+ <div class="form-row">
684
+ <div class="form-group">
685
+ <label for="gridWidth">Grid Width (Columns)</label>
686
+ <input type="number" id="gridWidth" class="form-input" value="10" min="1" max="50"
687
+ onchange="resizeGrid()">
688
+ </div>
689
+ <div class="form-group">
690
+ <label for="gridHeight">Grid Height (Rows)</label>
691
+ <input type="number" id="gridHeight" class="form-input" value="10" min="1" max="50"
692
+ onchange="resizeGrid()">
693
+ </div>
694
+ </div>
695
+ <div class="form-row"
696
+ style="margin-top: 1rem; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 1rem; align-items: flex-end;">
697
+ <div class="form-group" style="flex: 2; margin-bottom: 0;">
698
+ <label for="cellSize">Cell Zoom Slider</label>
699
+ <input type="range" id="cellSize" class="form-input" min="8" max="128" value="32"
700
+ oninput="updateZoom(this.value)">
701
+ </div>
702
+ <div class="form-group" style="flex: 1; margin-bottom: 0;">
703
+ <label for="cellSizeNum">Size (px)</label>
704
+ <input type="number" id="cellSizeNum" class="form-input" min="8" value="32"
705
+ oninput="updateZoom(this.value)">
706
+ </div>
707
+ </div>
708
+ </div>
709
+
710
+ <!-- Paint Palette Toolbar -->
711
+ <div class="toolbar">
712
+ <button class="tool-btn btn-outline" id="undoBtn" onclick="undo()" title="Undo (Ctrl+Z)" disabled>↩️
713
+ Undo</button>
714
+ <button class="tool-btn btn-outline" id="redoBtn" onclick="redo()" title="Redo (Ctrl+Y)" disabled>↪️
715
+ Redo</button>
716
+ <div style="width: 1px; height: 24px; background: rgba(255,255,255,0.1); margin: 0 0.25rem;"></div>
717
+ <button class="tool-btn active" data-tile="empty" onclick="selectTool(this)" title="[0] or [e]">🧱
718
+ Wall (0)</button>
719
+ <button class="tool-btn" data-tile="spawn" onclick="selectTool(this)" title="[1] or [s]">🟢 Spawn
720
+ (1)</button>
721
+ <button class="tool-btn" data-tile="deliver" onclick="selectTool(this)" title="[2] or [d]">🔴
722
+ Delivery (2)</button>
723
+ <button class="tool-btn" data-tile="normal" onclick="selectTool(this)" title="[3] or [w]">⬜
724
+ Pavement (3)</button>
725
+ <button class="tool-btn" data-tile="crate" onclick="selectTool(this)" title="[5] or [c]">📦 Crate
726
+ (5)</button>
727
+ <button class="tool-btn" data-tile="crate_spawn" onclick="selectTool(this)" title="[!] or [x]">🎯
728
+ Crate Spawn (5!)</button>
729
+ <button class="tool-btn" data-tile="↑" onclick="selectTool(this)" title="[Up Arrow] ↑">⬆️ Up
730
+ Arrow</button>
731
+ <button class="tool-btn" data-tile="→" onclick="selectTool(this)" title="[Right Arrow] →">➡️ Right
732
+ Arrow</button>
733
+ <button class="tool-btn" data-tile="↓" onclick="selectTool(this)" title="[Down Arrow] ↓">⬇️ Down
734
+ Arrow</button>
735
+ <button class="tool-btn" data-tile="←" onclick="selectTool(this)" title="[Left Arrow] ←">⬅️ Left
736
+ Arrow</button>
737
+ </div>
738
+
739
+ <!-- Grid Canvas Viewport -->
740
+ <div class="grid-container-card" id="gridContainerCard">
741
+ <div class="grid-wrapper" id="gridWrapper">
742
+ <div class="grid-canvas" id="gridCanvas">
743
+ <!-- Injected dynamically -->
744
+ </div>
745
+ </div>
746
+ </div>
747
+
748
+ <!-- Coordinates HUD -->
749
+ <div class="coord-overlay">
750
+ <div class="coord-item">Visual Coordinate: <span id="visualCoord">Row: 0, Col: 0</span></div>
751
+ <div class="coord-item">JSON Grid Coordinate: <span id="schemaCoord">X: 0, Y: 9</span></div>
752
+ </div>
753
+
754
+ <!-- Keyboard shortcuts cheatsheet -->
755
+ <div class="card">
756
+ <div class="card-title"
757
+ style="font-size: 1rem; border-bottom: none; margin-bottom: 0.75rem; padding-bottom: 0;">⌨️
758
+ Keyboard Shortcuts</div>
759
+ <div class="shortcuts-box">
760
+ <div class="shortcut-item"><span class="shortcut-key">0</span> / <span
761
+ class="shortcut-key">e</span> <span>Wall</span></div>
762
+ <div class="shortcut-item"><span class="shortcut-key">1</span> / <span
763
+ class="shortcut-key">s</span> <span>Spawn</span></div>
764
+ <div class="shortcut-item"><span class="shortcut-key">2</span> / <span
765
+ class="shortcut-key">d</span> <span>Delivery</span></div>
766
+ <div class="shortcut-item"><span class="shortcut-key">3</span> / <span
767
+ class="shortcut-key">w</span> <span>Pavement</span></div>
768
+ <div class="shortcut-item"><span class="shortcut-key">5</span> / <span
769
+ class="shortcut-key">c</span> <span>Crate</span></div>
770
+ <div class="shortcut-item"><span class="shortcut-key">!</span> / <span
771
+ class="shortcut-key">x</span> <span>Crate Spawn</span></div>
772
+ <div class="shortcut-item"><span class="shortcut-key">↑ ↓ ← →</span> <span>Arrows</span></div>
773
+ <div class="shortcut-item"><span class="shortcut-key">Ctrl+Z</span> <span>Undo</span></div>
774
+ <div class="shortcut-item"><span class="shortcut-key">Ctrl+Y</span> <span>Redo</span></div>
775
+ </div>
776
+ </div>
777
+
778
+ </div>
779
+
780
+ <!-- Right Column: Settings & JSON Code Editor -->
781
+ <div class="settings-side">
782
+
783
+ <!-- Game Settings Card -->
784
+ <div class="card">
785
+ <div class="card-title">
786
+ <span>⚙️</span> Game Settings
787
+ </div>
788
+ <div class="form-group">
789
+ <label for="mapTitle">Map Title</label>
790
+ <input type="text" id="mapTitle" class="form-input" value="custom_arena"
791
+ oninput="generateJson()">
792
+ </div>
793
+ <div class="form-group">
794
+ <label for="mapDesc">Map Description</label>
795
+ <textarea id="mapDesc" class="form-input" style="height: 60px; resize: vertical;"
796
+ oninput="generateJson()">Visually created map matching Deliveroo column-oriented coordination grid.</textarea>
797
+ </div>
798
+ <div class="form-row">
799
+ <div class="form-group">
800
+ <label for="maxPlayers">Max Players</label>
801
+ <input type="number" id="maxPlayers" class="form-input" value="4" min="1" max="100"
802
+ oninput="generateJson()">
803
+ </div>
804
+ <div class="form-group">
805
+ <label for="capacity">Player Cargo Capacity</label>
806
+ <input type="number" id="capacity" class="form-input" value="5" min="1" max="100"
807
+ oninput="generateJson()">
808
+ </div>
809
+ </div>
810
+ <div class="form-row">
811
+ <div class="form-group">
812
+ <label for="parcelMax">Max Parcels On Map</label>
813
+ <input type="number" id="parcelMax" class="form-input" value="5" min="0"
814
+ oninput="generateJson()">
815
+ </div>
816
+ <div class="form-group">
817
+ <label for="parcelGen">Parcel Spawn Rate</label>
818
+ <select id="parcelGen" class="form-input form-select" onchange="generateJson()">
819
+ <option value="1s">1s</option>
820
+ <option value="2s" selected>2s</option>
821
+ <option value="5s">5s</option>
822
+ <option value="10s">10s</option>
823
+ <option value="infinite">infinite</option>
824
+ </select>
825
+ </div>
826
+ </div>
827
+ <div class="form-row">
828
+ <div class="form-group">
829
+ <label for="movementDuration">Agent Move Duration (ms)</label>
830
+ <input type="number" id="movementDuration" class="form-input" value="50" min="0"
831
+ oninput="generateJson()">
832
+ </div>
833
+ <div class="form-group">
834
+ <label for="obsDistance">Observation Distance</label>
835
+ <input type="number" id="obsDistance" class="form-input" value="5" min="0"
836
+ oninput="generateJson()">
837
+ </div>
838
+ </div>
839
+ <div class="form-row">
840
+ <div class="form-group">
841
+ <label for="agentType">Agent Type</label>
842
+ <input type="text" id="agentType" class="form-input" value="" placeholder="default (undefined)"
843
+ oninput="generateJson()">
844
+ </div>
845
+ <div class="form-group">
846
+ <label for="parcelDecay">Parcel Decay Rate</label>
847
+ <input type="text" id="parcelDecay" class="form-input" value="1s" placeholder="e.g. 1s"
848
+ oninput="generateJson()">
849
+ </div>
850
+ </div>
851
+ <div class="form-row">
852
+ <div class="form-group">
853
+ <label for="rewardAvg">Parcel Reward Avg</label>
854
+ <input type="number" id="rewardAvg" class="form-input" value="30" min="0"
855
+ oninput="generateJson()">
856
+ </div>
857
+ <div class="form-group">
858
+ <label for="rewardVariance">Parcel Reward Var</label>
859
+ <input type="number" id="rewardVariance" class="form-input" value="10" min="0"
860
+ oninput="generateJson()">
861
+ </div>
862
+ </div>
863
+ </div>
864
+
865
+ <!-- NPC Configuration Card -->
866
+ <div class="card">
867
+ <div class="card-title" style="justify-content: space-between; display: flex; align-items: center;">
868
+ <span>🤖 NPCs Configuration</span>
869
+ <button class="btn" style="padding: 0.35rem 0.6rem; font-size: 0.75rem;" onclick="addNpcRow()">➕
870
+ Add NPC</button>
871
+ </div>
872
+
873
+ <div class="npc-list-container" id="npcList">
874
+ <!-- Dynamic NPC list -->
875
+ </div>
876
+ </div>
877
+
878
+ <!-- Editable Config JSON Card -->
879
+ <div class="card">
880
+ <div class="card-title">
881
+ <span>📄 Game Configuration JSON</span>
882
+ </div>
883
+
884
+ <!-- Validation output banner -->
885
+ <div id="jsonValidationStatus" class="validation-status validation-valid">
886
+ ✔️ Valid Configuration JSON
887
+ </div>
888
+
889
+ <div class="json-textarea-container">
890
+ <textarea id="jsonOutput" class="json-textarea" oninput="handleJsonTextareaInput()"></textarea>
891
+ </div>
892
+
893
+ <div style="margin-top: 1.25rem; display: flex; gap: 0.75rem; flex-wrap: wrap;">
894
+ <button class="btn" style="flex: 1; min-width: 90px;" onclick="copyJson()">📋 Copy</button>
895
+ <button class="btn btn-outline" style="flex: 1; min-width: 90px;" onclick="downloadJson()">💾
896
+ Download</button>
897
+ <button class="btn btn-outline" style="flex: 1; min-width: 90px;" onclick="toggleImport()">📥
898
+ Import</button>
899
+ </div>
900
+ <div id="importContainer"
901
+ style="display: none; margin-top: 1.25rem; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 1.25rem;">
902
+ <label for="importJsonInput">Paste Map JSON or Choose File</label>
903
+ <input type="file" id="mapFileSelect" accept=".json" class="form-input"
904
+ style="margin-bottom: 0.75rem;" onchange="handleMapFileSelect(this, 'importJsonInput')">
905
+ <textarea id="importJsonInput" class="form-input"
906
+ style="font-family: var(--font-mono); font-size: 0.8rem; height: 120px; resize: vertical;"
907
+ placeholder='{ "map": { "tiles": [...] } }'></textarea>
908
+ <button class="btn" style="margin-top: 0.75rem;" onclick="applyImport()">Apply Import</button>
909
+ </div>
910
+ </div>
911
+
912
+ </div>
913
+
914
+ </div>
915
+
916
+ <footer
917
+ style="margin-top: 3rem; border-top: 1px solid var(--border-color); padding-top: 1.5rem; text-align: center; font-size: 0.72rem; color: var(--text-muted); margin-bottom: 2rem;">
918
+ made by <a href="https://github.com/ale-bena" target="_blank"
919
+ style="color: var(--text-main); font-weight: 700; text-decoration: none; transition: color var(--transition-fast);"
920
+ onmouseover="this.style.color='var(--primary)'"
921
+ onmouseout="this.style.color='var(--text-main)'">Alessandro Benassi</a> and <a
922
+ href="https://github.com/xupremix" target="_blank"
923
+ style="color: var(--text-main); font-weight: 700; text-decoration: none; transition: color var(--transition-fast);"
924
+ onmouseover="this.style.color='var(--primary)'" onmouseout="this.style.color='var(--text-main)'">Lollato
925
+ Filippo</a>
926
+ </footer>
927
+
928
+ </div>
929
+
930
+ <script>
931
+ let gridWidth = 10;
932
+ let gridHeight = 10;
933
+ let activeTool = "empty";
934
+ let gridState = []; // 2D array representation of the visual grid: gridState[row][col]
935
+ let isMouseDown = false;
936
+ let undoStack = [];
937
+ let redoStack = [];
938
+ let skipJsonOutputUpdate = false;
939
+
940
+ // NPC State array: each entry is { type: "random", count: 1, moving_event: "frame" }
941
+ let npcState = [
942
+ { type: "random", count: 1, moving_event: "frame" }
943
+ ];
944
+
945
+ const displaySymbols = {
946
+ "empty": "0",
947
+ "normal": "3",
948
+ "spawn": "1",
949
+ "deliver": "2",
950
+ "crate_spawn": "5!",
951
+ "crate": "5"
952
+ };
953
+
954
+ const reverseSymbols = {
955
+ "0": "empty",
956
+ "3": "normal",
957
+ "1": "spawn",
958
+ "2": "deliver",
959
+ "5": "crate",
960
+ "5!": "crate_spawn"
961
+ };
962
+
963
+ // Hotkeys binding listeners
964
+ document.addEventListener('keydown', (e) => {
965
+ // Bypass hotkeys when focused on form inputs or textarea
966
+ const tag = document.activeElement.tagName;
967
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
968
+ return;
969
+ }
970
+
971
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
972
+ e.preventDefault();
973
+ undo();
974
+ } else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'y') {
975
+ e.preventDefault();
976
+ redo();
977
+ } else if (e.key === '0' || e.key === 'e') {
978
+ selectToolByAttribute('empty');
979
+ } else if (e.key === '3' || e.key === 'w') {
980
+ selectToolByAttribute('normal');
981
+ } else if (e.key === '1' || e.key === 's') {
982
+ selectToolByAttribute('spawn');
983
+ } else if (e.key === '2' || e.key === 'd') {
984
+ selectToolByAttribute('deliver');
985
+ } else if (e.key === '5' || e.key === 'c') {
986
+ selectToolByAttribute('crate');
987
+ } else if (e.key === '!' || e.key === 'x') {
988
+ selectToolByAttribute('crate_spawn');
989
+ } else if (e.key === 'ArrowUp') {
990
+ e.preventDefault();
991
+ selectToolByAttribute('↑');
992
+ } else if (e.key === 'ArrowRight') {
993
+ e.preventDefault();
994
+ selectToolByAttribute('→');
995
+ } else if (e.key === 'ArrowDown') {
996
+ e.preventDefault();
997
+ selectToolByAttribute('↓');
998
+ } else if (e.key === 'ArrowLeft') {
999
+ e.preventDefault();
1000
+ selectToolByAttribute('←');
1001
+ }
1002
+ });
1003
+
1004
+ function selectToolByAttribute(val) {
1005
+ const btn = document.querySelector(`.tool-btn[data-tile="${val}"]`);
1006
+ if (btn) {
1007
+ selectTool(btn);
1008
+ }
1009
+ }
1010
+
1011
+ function saveState() {
1012
+ const stateCopy = JSON.parse(JSON.stringify(gridState));
1013
+ undoStack.push(stateCopy);
1014
+ if (undoStack.length > 50) {
1015
+ undoStack.shift();
1016
+ }
1017
+ redoStack = [];
1018
+ updateHistoryButtons();
1019
+ }
1020
+
1021
+ function undo() {
1022
+ if (undoStack.length === 0) return;
1023
+ const currentState = JSON.parse(JSON.stringify(gridState));
1024
+ redoStack.push(currentState);
1025
+
1026
+ gridState = undoStack.pop();
1027
+ renderCanvas();
1028
+ generateJson();
1029
+ updateHistoryButtons();
1030
+ }
1031
+
1032
+ function redo() {
1033
+ if (redoStack.length === 0) return;
1034
+ const currentState = JSON.parse(JSON.stringify(gridState));
1035
+ undoStack.push(currentState);
1036
+
1037
+ gridState = redoStack.pop();
1038
+ renderCanvas();
1039
+ generateJson();
1040
+ updateHistoryButtons();
1041
+ }
1042
+
1043
+ function updateHistoryButtons() {
1044
+ const undoBtn = document.getElementById('undoBtn');
1045
+ const redoBtn = document.getElementById('redoBtn');
1046
+ if (undoBtn) undoBtn.disabled = (undoStack.length === 0);
1047
+ if (redoBtn) redoBtn.disabled = (redoStack.length === 0);
1048
+ }
1049
+
1050
+ function initGrid() {
1051
+ gridState = [];
1052
+ for (let r = 0; r < gridHeight; r++) {
1053
+ const row = [];
1054
+ for (let c = 0; c < gridWidth; c++) {
1055
+ row.push("normal");
1056
+ }
1057
+ gridState.push(row);
1058
+ }
1059
+
1060
+ // Default spawn and delivery cells
1061
+ if (gridHeight >= 3 && gridWidth >= 3) {
1062
+ gridState[1][1] = "spawn";
1063
+ gridState[gridHeight - 2][gridWidth - 2] = "deliver";
1064
+ } else {
1065
+ if (gridHeight > 0 && gridWidth > 0) {
1066
+ gridState[0][0] = "spawn";
1067
+ }
1068
+ if (gridHeight > 1 || gridWidth > 1) {
1069
+ gridState[gridHeight - 1][gridWidth - 1] = "deliver";
1070
+ }
1071
+ }
1072
+
1073
+ renderCanvas();
1074
+ renderNpcList();
1075
+ }
1076
+
1077
+ function selectTool(btn) {
1078
+ document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
1079
+ btn.classList.add('active');
1080
+ activeTool = btn.getAttribute('data-tile');
1081
+ }
1082
+
1083
+ function resizeGrid() {
1084
+ const newW = parseInt(document.getElementById('gridWidth').value) || 10;
1085
+ const newH = parseInt(document.getElementById('gridHeight').value) || 10;
1086
+
1087
+ const clampedW = Math.max(1, Math.min(50, newW));
1088
+ const clampedH = Math.max(1, Math.min(50, newH));
1089
+
1090
+ document.getElementById('gridWidth').value = clampedW;
1091
+ document.getElementById('gridHeight').value = clampedH;
1092
+
1093
+ // Preserve old gridState contents where overlapping
1094
+ const oldState = gridState;
1095
+ const oldH = gridHeight;
1096
+ const oldW = gridWidth;
1097
+
1098
+ gridWidth = clampedW;
1099
+ gridHeight = clampedH;
1100
+
1101
+ gridState = [];
1102
+ for (let r = 0; r < gridHeight; r++) {
1103
+ const row = [];
1104
+ for (let c = 0; c < gridWidth; c++) {
1105
+ if (r < oldH && c < oldW) {
1106
+ row.push(oldState[r][c]);
1107
+ } else {
1108
+ row.push("normal");
1109
+ }
1110
+ }
1111
+ gridState.push(row);
1112
+ }
1113
+
1114
+ renderCanvas();
1115
+ }
1116
+
1117
+ function updateZoom(val) {
1118
+ let parsed = parseInt(val) || 32;
1119
+ if (parsed < 8) parsed = 8;
1120
+
1121
+ document.getElementById('cellSize').value = Math.min(parsed, 128);
1122
+ document.getElementById('cellSizeNum').value = parsed;
1123
+
1124
+ const canvas = document.getElementById('gridCanvas');
1125
+ canvas.style.setProperty('--cell-size', `${parsed}px`);
1126
+
1127
+ adjustFlexBasis();
1128
+ }
1129
+
1130
+ function adjustFlexBasis() {
1131
+ const cellSize = parseInt(document.getElementById('cellSizeNum').value) || 32;
1132
+ const canvasWidth = gridWidth * cellSize + (gridWidth - 1) + 4 + 48; // cols * size + gaps + borders + padding
1133
+ const editorSide = document.querySelector('.editor-side');
1134
+ if (editorSide) {
1135
+ if (canvasWidth > 600) {
1136
+ editorSide.style.flexBasis = `${Math.min(canvasWidth, 1400)}px`;
1137
+ } else {
1138
+ editorSide.style.flexBasis = '600px';
1139
+ }
1140
+ }
1141
+ }
1142
+
1143
+ function handleCellAction(row, col) {
1144
+ gridState[row][col] = activeTool;
1145
+ updateCellClass(row, col);
1146
+ generateJson();
1147
+ }
1148
+
1149
+ function updateCellClass(row, col) {
1150
+ const cell = document.getElementById(`cell-${row}-${col}`);
1151
+ if (!cell) return;
1152
+
1153
+ cell.className = 'grid-cell';
1154
+ const value = gridState[row][col];
1155
+
1156
+ if (['↑', '→', '↓', '←'].includes(value)) {
1157
+ cell.classList.add('tile-arrow');
1158
+ cell.innerText = value;
1159
+ } else {
1160
+ cell.classList.add(`tile-${value}`);
1161
+ cell.innerText = displaySymbols[value] !== undefined ? displaySymbols[value] : value;
1162
+ }
1163
+ }
1164
+
1165
+ function handleHover(row, col) {
1166
+ const schemaX = col;
1167
+ const schemaY = gridHeight - 1 - row;
1168
+
1169
+ document.getElementById('visualCoord').innerText = `Row: ${row}, Col: ${col}`;
1170
+ document.getElementById('schemaCoord').innerText = `X: ${schemaX}, Y: ${schemaY}`;
1171
+ }
1172
+
1173
+ function renderCanvas() {
1174
+ const canvas = document.getElementById('gridCanvas');
1175
+ const zoomVal = document.getElementById('cellSizeNum').value;
1176
+ canvas.style.setProperty('--cell-size', `${zoomVal}px`);
1177
+ canvas.style.gridTemplateColumns = `repeat(${gridWidth}, var(--cell-size))`;
1178
+ canvas.innerHTML = '';
1179
+
1180
+ for (let r = 0; r < gridHeight; r++) {
1181
+ for (let c = 0; c < gridWidth; c++) {
1182
+ const cell = document.createElement('div');
1183
+ cell.id = `cell-${r}-${c}`;
1184
+ cell.className = 'grid-cell';
1185
+
1186
+ cell.addEventListener('mousedown', (e) => {
1187
+ saveState();
1188
+ isMouseDown = true;
1189
+ handleCellAction(r, c);
1190
+ });
1191
+ cell.addEventListener('mouseover', () => {
1192
+ handleHover(r, c);
1193
+ if (isMouseDown) {
1194
+ handleCellAction(r, c);
1195
+ }
1196
+ });
1197
+
1198
+ canvas.appendChild(cell);
1199
+ updateCellClass(r, c);
1200
+ }
1201
+ }
1202
+
1203
+ document.addEventListener('mouseup', () => {
1204
+ isMouseDown = false;
1205
+ });
1206
+
1207
+ generateJson();
1208
+ adjustFlexBasis();
1209
+ }
1210
+
1211
+ // --- NPC Dynamic List Form ---
1212
+ function renderNpcList() {
1213
+ const container = document.getElementById('npcList');
1214
+ container.innerHTML = '';
1215
+
1216
+ if (npcState.length === 0) {
1217
+ container.innerHTML = `<div style="font-size:0.8rem; color:var(--text-muted); text-align:center; padding:0.5rem;">No NPCs configured.</div>`;
1218
+ return;
1219
+ }
1220
+
1221
+ npcState.forEach((npc, index) => {
1222
+ const row = document.createElement('div');
1223
+ row.className = 'npc-row';
1224
+
1225
+ row.innerHTML = `
1226
+ <input type="text" class="form-input" style="flex: 2;" placeholder="Type (e.g. random)" value="${npc.type || ''}" oninput="updateNpc(${index}, 'type', this.value)">
1227
+ <input type="number" class="form-input" style="flex: 1.2;" placeholder="Count" min="0" value="${npc.count || 0}" oninput="updateNpc(${index}, 'count', this.value)">
1228
+ <input type="text" class="form-input" style="flex: 2;" placeholder="Event (e.g. frame)" value="${npc.moving_event || ''}" oninput="updateNpc(${index}, 'moving_event', this.value)">
1229
+ <button class="btn btn-outline btn-danger" style="flex-shrink: 0; padding: 0.5rem 0.6rem;" onclick="removeNpcRow(${index})" title="Delete NPC Group">🗑️</button>
1230
+ `;
1231
+
1232
+ container.appendChild(row);
1233
+ });
1234
+ }
1235
+
1236
+ function addNpcRow() {
1237
+ npcState.push({ type: 'random', count: 1, moving_event: 'frame' });
1238
+ renderNpcList();
1239
+ generateJson();
1240
+ }
1241
+
1242
+ function removeNpcRow(index) {
1243
+ npcState.splice(index, 1);
1244
+ renderNpcList();
1245
+ generateJson();
1246
+ }
1247
+
1248
+ function updateNpc(index, field, value) {
1249
+ if (field === 'count') {
1250
+ npcState[index][field] = parseInt(value) || 0;
1251
+ } else {
1252
+ npcState[index][field] = value;
1253
+ }
1254
+ generateJson();
1255
+ }
1256
+
1257
+ // --- JSON Generation ---
1258
+ function generateJson() {
1259
+ const title = document.getElementById('mapTitle').value || 'custom_arena';
1260
+ const description = document.getElementById('mapDesc').value || 'Visually created map matching Deliveroo column-oriented coordination grid.';
1261
+ const maxPlayers = parseInt(document.getElementById('maxPlayers').value) || 4;
1262
+ const capacity = parseInt(document.getElementById('capacity').value) || 5;
1263
+ const parcelMax = parseInt(document.getElementById('parcelMax').value) || 5;
1264
+ const parcelGen = document.getElementById('parcelGen').value || '2s';
1265
+
1266
+ // Custom configuration fields
1267
+ const movementDurationVal = document.getElementById('movementDuration').value;
1268
+ const movementDuration = movementDurationVal !== "" ? parseInt(movementDurationVal) : 50;
1269
+
1270
+ const obsDistanceVal = document.getElementById('obsDistance').value;
1271
+ const obsDistance = obsDistanceVal !== "" ? parseInt(obsDistanceVal) : 5;
1272
+
1273
+ const agentTypeInput = document.getElementById('agentType').value.trim();
1274
+ const agentType = agentTypeInput === "" ? undefined : agentTypeInput;
1275
+
1276
+ const parcelDecay = document.getElementById('parcelDecay').value || '1s';
1277
+
1278
+ const rewardAvgVal = document.getElementById('rewardAvg').value;
1279
+ const rewardAvg = rewardAvgVal !== "" ? parseInt(rewardAvgVal) : 30;
1280
+
1281
+ const rewardVarianceVal = document.getElementById('rewardVariance').value;
1282
+ const rewardVariance = rewardVarianceVal !== "" ? parseInt(rewardVarianceVal) : 10;
1283
+
1284
+ const tiles = [];
1285
+ for (let x = 0; x < gridWidth; x++) {
1286
+ const column = [];
1287
+ for (let y = 0; y < gridHeight; y++) {
1288
+ const visualRow = gridHeight - 1 - y;
1289
+ const visualCol = x;
1290
+ const val = gridState[visualRow][visualCol];
1291
+ column.push(displaySymbols[val] !== undefined ? displaySymbols[val] : val);
1292
+ }
1293
+ tiles.push(column);
1294
+ }
1295
+
1296
+ const formattedNpcs = npcState.map(npc => {
1297
+ const entry = { type: npc.type || 'random', count: npc.count || 0 };
1298
+ if (npc.moving_event) {
1299
+ entry.moving_event = npc.moving_event;
1300
+ }
1301
+ return entry;
1302
+ });
1303
+
1304
+ const payload = {
1305
+ title: title,
1306
+ description: description,
1307
+ maxPlayers: maxPlayers,
1308
+ map: {
1309
+ width: gridWidth,
1310
+ height: gridHeight,
1311
+ tiles: tiles
1312
+ },
1313
+ npcs: formattedNpcs,
1314
+ parcels: {
1315
+ generation_event: parcelGen,
1316
+ decaying_event: parcelDecay,
1317
+ max: parcelMax,
1318
+ reward_avg: rewardAvg,
1319
+ reward_variance: rewardVariance
1320
+ },
1321
+ player: {
1322
+ agent_type: agentType,
1323
+ movement_duration: movementDuration,
1324
+ observation_distance: obsDistance,
1325
+ capacity: capacity
1326
+ }
1327
+ };
1328
+
1329
+ if (!skipJsonOutputUpdate) {
1330
+ const textarea = document.getElementById('jsonOutput');
1331
+ textarea.value = JSON.stringify(payload, null, 2);
1332
+ }
1333
+
1334
+ setValidationStatus(true);
1335
+ }
1336
+
1337
+ // --- Bi-directional JSON Input Parsing & Validation ---
1338
+ function handleJsonTextareaInput() {
1339
+ const textarea = document.getElementById('jsonOutput');
1340
+ const raw = textarea.value.trim();
1341
+
1342
+ if (!raw) {
1343
+ setValidationStatus(false, "Textarea is empty.");
1344
+ return;
1345
+ }
1346
+
1347
+ try {
1348
+ const data = JSON.parse(raw);
1349
+
1350
+ // Run basic schema checking
1351
+ if (!data.map || typeof data.map.width === 'undefined' || typeof data.map.height === 'undefined' || !data.map.tiles) {
1352
+ setValidationStatus(false, "Schema validation failed:\nMissing 'map' fields (width, height, or tiles).");
1353
+ return;
1354
+ }
1355
+
1356
+ const width = parseInt(data.map.width);
1357
+ const height = parseInt(data.map.height);
1358
+
1359
+ if (isNaN(width) || isNaN(height) || width < 1 || height < 1 || width > 50 || height > 50) {
1360
+ setValidationStatus(false, `Schema validation failed:\nGrid dimensions must be integers between 1 and 50 (got width=${width}, height=${height}).`);
1361
+ return;
1362
+ }
1363
+
1364
+ const tiles = data.map.tiles;
1365
+ if (!Array.isArray(tiles) || tiles.length !== width) {
1366
+ setValidationStatus(false, `Schema validation failed:\n'map.tiles' must be an array representing columns. Length must equal width (${width}), got ${Array.isArray(tiles) ? tiles.length : typeof tiles}.`);
1367
+ return;
1368
+ }
1369
+
1370
+ for (let x = 0; x < width; x++) {
1371
+ if (!Array.isArray(tiles[x]) || tiles[x].length !== height) {
1372
+ setValidationStatus(false, `Schema validation failed:\nColumn tiles[${x}] must be an array of length height (${height}), got ${Array.isArray(tiles[x]) ? tiles[x].length : typeof tiles[x]}.`);
1373
+ return;
1374
+ }
1375
+ }
1376
+
1377
+ // If valid, sync settings and canvas state
1378
+ saveState();
1379
+
1380
+ // Set flag to avoid overwriting textarea while typing
1381
+ skipJsonOutputUpdate = true;
1382
+
1383
+ gridWidth = width;
1384
+ gridHeight = height;
1385
+
1386
+ document.getElementById('gridWidth').value = gridWidth;
1387
+ document.getElementById('gridHeight').value = gridHeight;
1388
+
1389
+ if (data.title) document.getElementById('mapTitle').value = data.title;
1390
+ if (data.description) document.getElementById('mapDesc').value = data.description;
1391
+ if (data.maxPlayers) document.getElementById('maxPlayers').value = data.maxPlayers;
1392
+ if (data.player && data.player.capacity) document.getElementById('capacity').value = data.player.capacity;
1393
+ if (data.parcels && data.parcels.max) document.getElementById('parcelMax').value = data.parcels.max;
1394
+
1395
+ // Sync new configuration fields
1396
+ if (data.player && data.player.movement_duration !== undefined) {
1397
+ document.getElementById('movementDuration').value = data.player.movement_duration;
1398
+ }
1399
+ if (data.player && data.player.observation_distance !== undefined) {
1400
+ document.getElementById('obsDistance').value = data.player.observation_distance;
1401
+ }
1402
+ if (data.player && data.player.agent_type !== undefined) {
1403
+ document.getElementById('agentType').value = data.player.agent_type;
1404
+ } else {
1405
+ document.getElementById('agentType').value = '';
1406
+ }
1407
+ if (data.parcels && data.parcels.decaying_event !== undefined) {
1408
+ document.getElementById('parcelDecay').value = data.parcels.decaying_event;
1409
+ }
1410
+ if (data.parcels && data.parcels.reward_avg !== undefined) {
1411
+ document.getElementById('rewardAvg').value = data.parcels.reward_avg;
1412
+ }
1413
+ if (data.parcels && data.parcels.reward_variance !== undefined) {
1414
+ document.getElementById('rewardVariance').value = data.parcels.reward_variance;
1415
+ }
1416
+
1417
+ // Sync parcel spawn rate select dropdown
1418
+ if (data.parcels && data.parcels.generation_event) {
1419
+ const genEvent = String(data.parcels.generation_event).trim().toLowerCase();
1420
+ const dropdown = document.getElementById('parcelGen');
1421
+ if (['1s', '2s', '5s', '10s', 'infinite'].includes(genEvent)) {
1422
+ dropdown.value = genEvent;
1423
+ } else {
1424
+ dropdown.value = '2s';
1425
+ }
1426
+ }
1427
+
1428
+ // Sync NPCs
1429
+ if (Array.isArray(data.npcs)) {
1430
+ npcState = data.npcs.map(n => ({
1431
+ type: n.type || 'random',
1432
+ count: parseInt(n.count) || 0,
1433
+ moving_event: n.moving_event || ''
1434
+ }));
1435
+ } else {
1436
+ npcState = [];
1437
+ }
1438
+ renderNpcList();
1439
+
1440
+ // Re-build gridState from column tiles
1441
+ gridState = [];
1442
+ for (let r = 0; r < gridHeight; r++) {
1443
+ const row = [];
1444
+ for (let c = 0; c < gridWidth; c++) {
1445
+ row.push("normal");
1446
+ }
1447
+ gridState.push(row);
1448
+ }
1449
+
1450
+ for (let x = 0; x < gridWidth; x++) {
1451
+ for (let y = 0; y < gridHeight; y++) {
1452
+ const visualRow = gridHeight - 1 - y;
1453
+ const visualCol = x;
1454
+ const rawVal = String(tiles[x][y]);
1455
+ gridState[visualRow][visualCol] = reverseSymbols[rawVal] || rawVal || "normal";
1456
+ }
1457
+ }
1458
+
1459
+ renderCanvas();
1460
+ setValidationStatus(true);
1461
+
1462
+ } catch (err) {
1463
+ setValidationStatus(false, `JSON Syntax Error:\n${err.message}`);
1464
+ } finally {
1465
+ skipJsonOutputUpdate = false;
1466
+ }
1467
+ }
1468
+
1469
+ function setValidationStatus(isValid, errorMessage = "") {
1470
+ const banner = document.getElementById('jsonValidationStatus');
1471
+ if (isValid) {
1472
+ banner.className = "validation-status validation-valid";
1473
+ banner.innerText = "✔️ Valid Configuration JSON";
1474
+ } else {
1475
+ banner.className = "validation-status validation-invalid";
1476
+ banner.innerText = errorMessage;
1477
+ }
1478
+ }
1479
+
1480
+ // --- Actions ---
1481
+ function downloadJson() {
1482
+ const text = document.getElementById('jsonOutput').value;
1483
+ const title = document.getElementById('mapTitle').value || 'custom_arena';
1484
+ const blob = new Blob([text], { type: 'application/json' });
1485
+ const url = URL.createObjectURL(blob);
1486
+ const a = document.createElement('a');
1487
+ a.href = url;
1488
+ a.download = `${title}.json`;
1489
+ document.body.appendChild(a);
1490
+ a.click();
1491
+ document.body.removeChild(a);
1492
+ URL.revokeObjectURL(url);
1493
+ }
1494
+
1495
+ function copyJson() {
1496
+ const text = document.getElementById('jsonOutput').value;
1497
+ navigator.clipboard.writeText(text).then(() => {
1498
+ showNotification("Game Configuration JSON copied to clipboard!", "success");
1499
+ });
1500
+ }
1501
+
1502
+ // --- Import Helper Functions ---
1503
+ function toggleImport() {
1504
+ const container = document.getElementById('importContainer');
1505
+ if (container.style.display === 'none') {
1506
+ container.style.display = 'block';
1507
+ } else {
1508
+ container.style.display = 'none';
1509
+ }
1510
+ }
1511
+
1512
+ function handleMapFileSelect(input, textareaId) {
1513
+ const file = input.files[0];
1514
+ if (!file) return;
1515
+
1516
+ const reader = new FileReader();
1517
+ reader.onload = function (e) {
1518
+ document.getElementById(textareaId).value = e.target.result;
1519
+ };
1520
+ reader.readAsText(file);
1521
+ }
1522
+
1523
+ function applyImport() {
1524
+ const rawJson = document.getElementById('importJsonInput').value.trim();
1525
+ if (!rawJson) return;
1526
+
1527
+ try {
1528
+ const data = JSON.parse(rawJson);
1529
+
1530
+ if (!data.map || typeof data.map.width === 'undefined' || typeof data.map.height === 'undefined' || !data.map.tiles) {
1531
+ showNotification("Invalid format! Must contain map.width, map.height, and map.tiles.", "error");
1532
+ return;
1533
+ }
1534
+
1535
+ saveState();
1536
+
1537
+ gridWidth = data.map.width;
1538
+ gridHeight = data.map.height;
1539
+
1540
+ document.getElementById('gridWidth').value = gridWidth;
1541
+ document.getElementById('gridHeight').value = gridHeight;
1542
+
1543
+ if (data.title) document.getElementById('mapTitle').value = data.title;
1544
+ if (data.description) document.getElementById('mapDesc').value = data.description;
1545
+ if (data.maxPlayers) document.getElementById('maxPlayers').value = data.maxPlayers;
1546
+ if (data.player && data.player.capacity) document.getElementById('capacity').value = data.player.capacity;
1547
+ if (data.parcels && data.parcels.max) document.getElementById('parcelMax').value = data.parcels.max;
1548
+
1549
+ // Sync new configuration fields
1550
+ if (data.player && data.player.movement_duration !== undefined) {
1551
+ document.getElementById('movementDuration').value = data.player.movement_duration;
1552
+ }
1553
+ if (data.player && data.player.observation_distance !== undefined) {
1554
+ document.getElementById('obsDistance').value = data.player.observation_distance;
1555
+ }
1556
+ if (data.player && data.player.agent_type !== undefined) {
1557
+ document.getElementById('agentType').value = data.player.agent_type;
1558
+ } else {
1559
+ document.getElementById('agentType').value = '';
1560
+ }
1561
+ if (data.parcels && data.parcels.decaying_event !== undefined) {
1562
+ document.getElementById('parcelDecay').value = data.parcels.decaying_event;
1563
+ }
1564
+ if (data.parcels && data.parcels.reward_avg !== undefined) {
1565
+ document.getElementById('rewardAvg').value = data.parcels.reward_avg;
1566
+ }
1567
+ if (data.parcels && data.parcels.reward_variance !== undefined) {
1568
+ document.getElementById('rewardVariance').value = data.parcels.reward_variance;
1569
+ }
1570
+
1571
+ if (data.parcels && data.parcels.generation_event) {
1572
+ const genEvent = String(data.parcels.generation_event).trim().toLowerCase();
1573
+ const dropdown = document.getElementById('parcelGen');
1574
+ if (['1s', '2s', '5s', '10s', 'infinite'].includes(genEvent)) {
1575
+ dropdown.value = genEvent;
1576
+ } else {
1577
+ dropdown.value = '2s';
1578
+ }
1579
+ }
1580
+
1581
+ if (Array.isArray(data.npcs)) {
1582
+ npcState = data.npcs.map(n => ({
1583
+ type: n.type || 'random',
1584
+ count: parseInt(n.count) || 0,
1585
+ moving_event: n.moving_event || ''
1586
+ }));
1587
+ } else {
1588
+ npcState = [];
1589
+ }
1590
+ renderNpcList();
1591
+
1592
+ gridState = [];
1593
+ for (let r = 0; r < gridHeight; r++) {
1594
+ const row = [];
1595
+ for (let c = 0; c < gridWidth; c++) {
1596
+ row.push("normal");
1597
+ }
1598
+ gridState.push(row);
1599
+ }
1600
+
1601
+ const localReverseSymbols = {
1602
+ "0": "empty",
1603
+ "3": "normal",
1604
+ "1": "spawn",
1605
+ "2": "deliver",
1606
+ "5": "crate",
1607
+ "5!": "crate_spawn"
1608
+ };
1609
+
1610
+ for (let x = 0; x < gridWidth; x++) {
1611
+ for (let y = 0; y < gridHeight; y++) {
1612
+ const visualRow = gridHeight - 1 - y;
1613
+ const visualCol = x;
1614
+ const rawVal = String(data.map.tiles[x][y]);
1615
+ gridState[visualRow][visualCol] = localReverseSymbols[rawVal] || rawVal || "normal";
1616
+ }
1617
+ }
1618
+
1619
+ renderCanvas();
1620
+ document.getElementById('importContainer').style.display = 'none';
1621
+ document.getElementById('importJsonInput').value = '';
1622
+ showNotification("Map imported successfully!", "success");
1623
+
1624
+ } catch (err) {
1625
+ showNotification("JSON parsing error: " + err.message, "error");
1626
+ }
1627
+ }
1628
+
1629
+ function showNotification(message, type = 'success') {
1630
+ let container = document.getElementById('toast-container');
1631
+ if (!container) {
1632
+ container = document.createElement('div');
1633
+ container.id = 'toast-container';
1634
+ container.className = 'toast-container';
1635
+ document.body.appendChild(container);
1636
+ }
1637
+
1638
+ const toast = document.createElement('div');
1639
+ toast.className = `toast ${type}`;
1640
+
1641
+ let icon = '✨';
1642
+ if (type === 'success') icon = '✅';
1643
+ if (type === 'error') icon = '❌';
1644
+
1645
+ toast.innerHTML = `
1646
+ <span class="toast-icon">${icon}</span>
1647
+ <span class="toast-message">${message}</span>
1648
+ `;
1649
+
1650
+ container.appendChild(toast);
1651
+
1652
+ // Trigger animation
1653
+ setTimeout(() => {
1654
+ toast.classList.add('show');
1655
+ }, 10);
1656
+
1657
+ // Remove toast after duration
1658
+ setTimeout(() => {
1659
+ toast.classList.remove('show');
1660
+ toast.addEventListener('transitionend', () => {
1661
+ toast.remove();
1662
+ });
1663
+ }, 3500);
1664
+ }
1665
+
1666
+ // Initialize grid on load
1667
+ window.onload = () => {
1668
+ initGrid();
1669
+ window.addEventListener('resize', adjustFlexBasis);
1670
+ };
1671
+ </script>
1672
+ </body>
1673
+
1674
+ </html>