scad-gltf 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2532 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Procedural Generator for "kids_digit_writing"
5
- * Godot 4 Kids Digit Writing Learning App with 3D Teacher Mouse Robot
6
- * Calibrated lighting, soft PBR toy materials, and optimized 2D particle pipelines
7
- */
8
-
9
- const fs = require("fs");
10
- const path = require("path");
11
-
12
- const ROOT_DIR = path.join(process.cwd(), "kids_digit_writing");
13
-
14
- function ensureDir(dirPath) {
15
- if (!fs.existsSync(dirPath)) {
16
- fs.mkdirSync(dirPath, { recursive: true });
17
- }
18
- }
19
-
20
- function writeFile(relativePath, content) {
21
- const fullPath = path.join(ROOT_DIR, relativePath);
22
- ensureDir(path.dirname(fullPath));
23
- fs.writeFileSync(fullPath, content.trim() + "\n", "utf8");
24
- console.log(` [CREATED] ${relativePath}`);
25
- }
26
-
27
- console.log("Generating Godot 4 Project: kids_digit_writing ...\n");
28
-
29
- // -------------------------------------------------------------------------
30
- // 1. ADDON: godot/addons/scad_importer/*
31
- // -------------------------------------------------------------------------
32
-
33
- writeFile(
34
- "addons/scad_importer/plugin.cfg",
35
- `
36
- [plugin]
37
-
38
- name="OpenSCAD GLTF Importer"
39
- description="Imports .scad files directly as 3D scenes using scad-gltf"
40
- author="Ilia Grigorev"
41
- version="0.1"
42
- script="scad_plugin.gd"
43
- license="MIT"
44
- `,
45
- );
46
-
47
- writeFile(
48
- "addons/scad_importer/scad_importer.gd",
49
- `# Copyright (c) 2026 Ilia Grigorev. Distributed under the MIT License.
50
- # See LICENSE in the addon directory for details.
51
-
52
- @tool
53
- extends EditorSceneFormatImporter
54
-
55
- func _get_extensions():
56
- return PackedStringArray(["scad"])
57
-
58
- func _get_import_flags():
59
- return EditorSceneFormatImporter.IMPORT_SCENE
60
-
61
- func _import_scene(path: String, flags: int, options: Dictionary) -> Object:
62
- var global_source = ProjectSettings.globalize_path(path)
63
- var unique_id = str(hash(path))
64
- var temp_glb_path = ProjectSettings.globalize_path("user://scad_cache_" + unique_id + ".glb")
65
-
66
- var args = PackedStringArray()
67
- args.append(global_source)
68
- args.append(temp_glb_path)
69
-
70
- var output = []
71
- print("Importing %s via scad-convert..." % path.get_file())
72
-
73
- var exit_code = -1
74
- if OS.get_name() == "Windows":
75
- var win_args = PackedStringArray(["/c", "scad-convert"])
76
- win_args.append_array(args)
77
- exit_code = OS.execute("cmd.exe", win_args, output, true)
78
- else:
79
- exit_code = OS.execute("scad-convert", args, output, true)
80
-
81
- if exit_code != 0:
82
- print("scad-convert conversion failed for %s. Attempting fallback to local scad-serve..." % path.get_file())
83
- var fallback_success = _try_scad_serve_fallback(global_source, temp_glb_path)
84
-
85
- if not fallback_success:
86
- push_error("Failed to compile SCAD file: %s. Ensure Node.js is installed or scad-serve is running." % path.get_file())
87
- push_error("scad-convert output: ", "\\n".join(output))
88
- return null
89
-
90
- var gltf_doc = GLTFDocument.new()
91
- var gltf_state = GLTFState.new()
92
- var err = gltf_doc.append_from_file(temp_glb_path, gltf_state)
93
-
94
- if FileAccess.file_exists(temp_glb_path):
95
- DirAccess.remove_absolute(temp_glb_path)
96
-
97
- if err != OK:
98
- push_error("Failed to parse the generated GLB for %s." % path.get_file())
99
- return null
100
-
101
- var generated_scene = gltf_doc.generate_scene(gltf_state)
102
- if generated_scene:
103
- generated_scene.name = path.get_file().get_basename()
104
-
105
- return generated_scene
106
-
107
- func _get_relative_path(base: String, target: String) -> String:
108
- var base_parts = base.replace("\\\\", "/").split("/", false)
109
- var target_parts = target.replace("\\\\", "/").split("/", false)
110
-
111
- if OS.get_name() == "Windows":
112
- if base_parts.size() > 0 and target_parts.size() > 0:
113
- if base_parts[0].nocasecmp_to(target_parts[0]) != 0:
114
- return target
115
-
116
- var common_count = 0
117
- var min_len = min(base_parts.size(), target_parts.size())
118
- for i in range(min_len):
119
- if base_parts[i].nocasecmp_to(target_parts[i]) == 0:
120
- common_count += 1
121
- else:
122
- break
123
-
124
- var rel_parts = PackedStringArray()
125
- for i in range(common_count, base_parts.size()):
126
- rel_parts.append("..")
127
-
128
- for i in range(common_count, target_parts.size()):
129
- rel_parts.append(target_parts[i])
130
-
131
- return "/".join(rel_parts)
132
-
133
- func _get_dependencies_recursive(file_path: String, visited: Dictionary) -> void:
134
- if visited.has(file_path):
135
- return
136
-
137
- visited[file_path] = ""
138
-
139
- if not FileAccess.file_exists(file_path):
140
- return
141
-
142
- var file = FileAccess.open(file_path, FileAccess.READ)
143
- if not file:
144
- return
145
-
146
- var content = file.get_as_text()
147
- file.close()
148
-
149
- visited[file_path] = content
150
-
151
- var regex = RegEx.new()
152
- regex.compile("(?:include|use)\\\\s*[<\\"]([^>\\"]+)[>\\"]")
153
-
154
- var base_dir = file_path.get_base_dir()
155
- for result in regex.search_all(content):
156
- var dep_rel_path = result.get_string(1)
157
- var dep_abs_path = base_dir.path_join(dep_rel_path).simplify_path()
158
- _get_dependencies_recursive(dep_abs_path, visited)
159
-
160
- func _get_dependencies(file_path: String) -> Dictionary:
161
- var visited = {}
162
- _get_dependencies_recursive(file_path, visited)
163
- return visited
164
-
165
- func _try_scad_serve_fallback(source_path: String, out_glb_path: String) -> bool:
166
- var deps = _get_dependencies(source_path)
167
- var content = deps.get(source_path, "")
168
- deps.erase(source_path)
169
-
170
- if content == "":
171
- return false
172
-
173
- var additional_files = {}
174
- var base_dir = source_path.get_base_dir()
175
- for dep_path in deps.keys():
176
- var rel_path = _get_relative_path(base_dir, dep_path)
177
- additional_files[rel_path] = deps[dep_path]
178
-
179
- var http = HTTPClient.new()
180
- var err = http.connect_to_host("127.0.0.1", 3000)
181
- if err != OK:
182
- return false
183
-
184
- var max_wait = 500
185
- var wait = 0
186
- while http.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]:
187
- http.poll()
188
- OS.delay_msec(10)
189
- wait += 1
190
- if wait > max_wait:
191
- return false
192
-
193
- if http.get_status() != HTTPClient.STATUS_CONNECTED:
194
- return false
195
-
196
- var headers = PackedStringArray(["Content-Type: application/json"])
197
- var payload = {
198
- "content": content,
199
- "options": {
200
- "additionalFiles": additional_files
201
- }
202
- }
203
-
204
- var body = JSON.stringify(payload)
205
- err = http.request(HTTPClient.METHOD_POST, "/api/convert", headers, body)
206
- if err != OK:
207
- return false
208
-
209
- max_wait = 6000
210
- wait = 0
211
- while http.get_status() == HTTPClient.STATUS_REQUESTING:
212
- http.poll()
213
- OS.delay_msec(10)
214
- wait += 1
215
- if wait > max_wait:
216
- return false
217
-
218
- if http.has_response() and http.get_response_code() == 200:
219
- var rb = PackedByteArray()
220
- while http.get_status() == HTTPClient.STATUS_BODY:
221
- http.poll()
222
- var chunk = http.read_response_body_chunk()
223
- if chunk.size() == 0:
224
- OS.delay_msec(10)
225
- else:
226
- rb.append_array(chunk)
227
-
228
- if rb.is_empty():
229
- return false
230
-
231
- var out_file = FileAccess.open(out_glb_path, FileAccess.WRITE)
232
- if not out_file:
233
- return false
234
- out_file.store_buffer(rb)
235
- out_file.close()
236
-
237
- print("Successfully compiled %s using scad-serve fallback." % source_path.get_file())
238
- return true
239
-
240
- return false
241
- `,
242
- );
243
-
244
- writeFile(
245
- "addons/scad_importer/scad_plugin.gd",
246
- `# Copyright (c) 2026 Ilia Grigorev. Distributed under the MIT License.
247
- # See LICENSE in the addon directory for details.
248
-
249
- @tool
250
- extends EditorPlugin
251
-
252
- var import_plugin
253
-
254
- func _enter_tree():
255
- import_plugin = preload("res://addons/scad_importer/scad_importer.gd").new()
256
- add_scene_format_importer_plugin(import_plugin)
257
-
258
- func _exit_tree():
259
- remove_scene_format_importer_plugin(import_plugin)
260
- import_plugin = null
261
- `,
262
- );
263
-
264
- // -------------------------------------------------------------------------
265
- // 2. 3D MODELS (.scad)
266
- // -------------------------------------------------------------------------
267
-
268
- writeFile(
269
- "assets/models/mouse_robot.scad",
270
- `/* Model Name: mouse_robot */
271
- // Note on Units: 1 unit ≈ 10 mm (1 cm). Total robot height is ~28 units (28 cm).
272
- $fn = 36;
273
- $asa = 45.0;
274
-
275
- // -------------------------------------------------------------
276
- // PBR Materials (Calibrated Anti-Glare Toy Enamel Shading)
277
- // -------------------------------------------------------------
278
- module mat_silver() {
279
- color([0.65, 0.72, 0.80], metalness=0.08, roughness=0.52, $asa=45) children();
280
- }
281
-
282
- module mat_cyan() {
283
- color([0.06, 0.66, 0.82], metalness=0.08, roughness=0.45, $asa=45) children();
284
- }
285
-
286
- module mat_blue_rim() {
287
- color([0.10, 0.32, 0.75], metalness=0.15, roughness=0.45, $asa=45) children();
288
- }
289
-
290
- module mat_magenta() {
291
- color([0.86, 0.16, 0.52], metalness=0.05, roughness=0.45, $asa=45) children();
292
- }
293
-
294
- module mat_purple() {
295
- color([0.50, 0.14, 0.62], metalness=0.08, roughness=0.50, $asa=45) children();
296
- }
297
-
298
- module mat_tan() {
299
- color([0.80, 0.64, 0.44], metalness=0.05, roughness=0.60, $asa=45) children();
300
- }
301
-
302
- module mat_black() {
303
- color([0.12, 0.13, 0.16], metalness=0.10, roughness=0.65, $asa=45) children();
304
- }
305
-
306
- module mat_white() {
307
- color([0.82, 0.84, 0.87], metalness=0.0, roughness=0.45, $asa=45) children();
308
- }
309
-
310
- module mat_dark_blue() {
311
- color([0.08, 0.18, 0.45], metalness=0.15, roughness=0.45, $asa=45) children();
312
- }
313
-
314
- // -------------------------------------------------------------
315
- // Large Round Ears (Facing Forward along +Y)
316
- // -------------------------------------------------------------
317
- module robot_ear() {
318
- rotate([90, 0, 0]) {
319
- mat_cyan() {
320
- difference() {
321
- cylinder(r=4.8, h=0.9, center=true);
322
- cylinder(r=3.8, h=1.2, center=true);
323
- }
324
- }
325
- mat_magenta() {
326
- cylinder(r=3.9, h=0.7, center=true);
327
- }
328
- mat_silver() {
329
- translate([0, -4.6, 0])
330
- cylinder(r=0.6, h=1.6, center=true);
331
- }
332
- }
333
- }
334
-
335
- // -------------------------------------------------------------
336
- // Robot Body Core & Face
337
- // -------------------------------------------------------------
338
- module robot_body() {
339
- mat_silver() {
340
- intersection() {
341
- scale([1.0, 0.92, 1.08]) sphere(r=8.0);
342
- translate([-10, -10, 1.0]) cube([20, 20, 10]);
343
- }
344
- }
345
-
346
- mat_silver() {
347
- translate([0, 0, 8.5]) cylinder(r=0.45, h=4.2);
348
- translate([0, 0, 13.0]) sphere(r=1.05);
349
- }
350
-
351
- mat_cyan() {
352
- intersection() {
353
- scale([1.01, 0.93, 1.09]) sphere(r=8.0);
354
- translate([-10, -10, -1.6]) cube([20, 20, 2.8]);
355
- }
356
- }
357
-
358
- mat_black() {
359
- intersection() {
360
- scale([1.03, 0.95, 1.11]) sphere(r=8.05);
361
- translate([-10, -10, -3.4]) cube([20, 20, 2.0]);
362
- }
363
- translate([0, 7.7, -2.4]) rotate([78, 0, 0])
364
- cylinder(r=0.9, h=0.7, center=true);
365
- }
366
-
367
- mat_purple() {
368
- intersection() {
369
- scale([1.0, 0.92, 1.08]) sphere(r=7.95);
370
- translate([-10, -10, -10]) cube([20, 20, 7.0]);
371
- }
372
- }
373
-
374
- for (s = [-1, 1]) {
375
- translate([s * 2.8, 6.4, 5.2]) rotate([-6, s * 8, 0]) {
376
- mat_white() sphere(r=1.25);
377
- mat_dark_blue() translate([0, 0.68, 0]) sphere(r=0.68);
378
- mat_white() translate([s * 0.3, 1.15, 0.3]) sphere(r=0.25);
379
-
380
- mat_dark_blue() {
381
- for (a = [-50, 0, 50]) {
382
- rotate([0, a, 0]) translate([0, 0.7, 1.25])
383
- cylinder(r=0.14, h=1.4);
384
- }
385
- }
386
- }
387
- }
388
-
389
- mat_dark_blue() {
390
- translate([0, 7.35, 4.2]) sphere(r=0.55);
391
- }
392
-
393
- mat_dark_blue() {
394
- for (i = [-5 : 5]) {
395
- let (
396
- u = i / 5.0,
397
- x = u * 1.15,
398
- z = 3.0 + (u * u) * 0.35,
399
- y = 7.55 - (u * u) * 0.15
400
- ) {
401
- translate([x, y, z]) sphere(r=0.18);
402
- }
403
- }
404
- }
405
-
406
- mat_white() {
407
- translate([-0.30, 7.58, 2.55]) cube([0.26, 0.28, 0.55], center=true);
408
- translate([0.30, 7.58, 2.55]) cube([0.26, 0.28, 0.55], center=true);
409
- }
410
-
411
- translate([6.8, 0.2, 7.5]) rotate([0, 18, 0]) robot_ear();
412
- translate([-6.8, 0.2, 7.5]) rotate([0, -18, 0]) robot_ear();
413
- }
414
-
415
- // -------------------------------------------------------------
416
- // Arm Segments & 5 Spread Fan Wire Fingers
417
- // -------------------------------------------------------------
418
- module arm_upper() {
419
- mat_cyan() sphere(r=1.3);
420
- mat_cyan() {
421
- translate([0, 0, -1.4]) cylinder(r=1.1, h=2.4, center=true);
422
- }
423
- mat_tan() {
424
- translate([0, 0, -2.8]) sphere(r=0.9);
425
- }
426
- }
427
-
428
- module arm_forearm() {
429
- mat_tan() {
430
- translate([0, 0, -1.3]) cylinder(r=0.85, h=2.2, center=true);
431
- translate([0, 0, -2.6]) sphere(r=0.85);
432
- }
433
- mat_dark_blue() {
434
- translate([0, 0, -2.8]) {
435
- cylinder(r=0.85, h=0.45, center=true);
436
- for (f = [-60, -30, 0, 30, 60]) {
437
- rotate([0, f, 0]) {
438
- translate([0, 0, -1.7]) cylinder(r=0.18, h=3.4, center=true);
439
- translate([0, 0, -3.4]) sphere(r=0.32);
440
- }
441
- }
442
- }
443
- }
444
- }
445
-
446
- // -------------------------------------------------------------
447
- // Leg Segments with Seamless Embedded Toe Claws
448
- // -------------------------------------------------------------
449
- module upper_leg() {
450
- mat_purple() {
451
- sphere(r=1.25);
452
- translate([0, 0, -1.8]) cylinder(r=1.15, h=2.0, center=true);
453
- }
454
- mat_tan() {
455
- translate([0, 0, -3.0]) sphere(r=0.9);
456
- }
457
- }
458
-
459
- module lower_leg() {
460
- mat_tan() {
461
- translate([0, 0, -1.8]) cylinder(r=0.85, h=2.2, center=true);
462
- }
463
- mat_cyan() {
464
- translate([0, 0, -3.2]) sphere(r=1.05);
465
- translate([0, 1.0, -3.8]) scale([1.0, 1.6, 0.75]) sphere(r=1.15);
466
-
467
- translate([0, 1.8, -4.15])
468
- rotate([-85, 0, 0]) cylinder(r1=0.36, r2=0.06, h=1.4);
469
- translate([0.48, 1.7, -4.15])
470
- rotate([-85, -12, 0]) cylinder(r1=0.34, r2=0.06, h=1.35);
471
- translate([-0.48, 1.7, -4.15])
472
- rotate([-85, 12, 0]) cylinder(r1=0.34, r2=0.06, h=1.35);
473
- }
474
- mat_blue_rim() {
475
- translate([0, -0.4, -4.1]) cylinder(r=0.45, h=0.7);
476
- }
477
- }
478
-
479
- // -------------------------------------------------------------
480
- // Teacher Animations
481
- // -------------------------------------------------------------
482
- robot_anim = [
483
- ["Idle", [
484
- ["Root", [
485
- [0.0, [ -5, 0, 0], [0, 0, 13.0]],
486
- [1.0, [ -3, 0, 0], [0, 0, 13.4]],
487
- [2.0, [ -5, 0, 0], [0, 0, 13.0]]
488
- ]],
489
- ["LeftArm", [
490
- [0.0, [ 15, -30, 0]],
491
- [1.0, [ 12, -32, 0]],
492
- [2.0, [ 15, -30, 0]]
493
- ]],
494
- ["LeftForearm", [
495
- [0.0, [ 30, 0, 0]],
496
- [1.0, [ 35, 0, 0]],
497
- [2.0, [ 30, 0, 0]]
498
- ]],
499
- ["RightArm", [
500
- [0.0, [ 15, 30, 0]],
501
- [1.0, [ 12, 32, 0]],
502
- [2.0, [ 15, 30, 0]]
503
- ]],
504
- ["RightForearm", [
505
- [0.0, [ 30, 0, 0]],
506
- [1.0, [ 35, 0, 0]],
507
- [2.0, [ 30, 0, 0]]
508
- ]],
509
- ["LeftLeg", [ [0.0, [ 15, 0, 6]] ]],
510
- ["LeftShin", [ [0.0, [-15, 0, 0]] ]],
511
- ["RightLeg", [ [0.0, [ 15, 0, -6]] ]],
512
- ["RightShin", [ [0.0, [-15, 0, 0]] ]]
513
- ]],
514
-
515
- ["Praise", [
516
- ["Root", [
517
- [0.0, [ -5, 0, 0], [0, 0, 13.0]],
518
- [0.3, [ 0, 0, 0], [0, 0, 17.0]],
519
- [0.6, [ -5, 0, 0], [0, 0, 13.0]],
520
- [0.9, [ 0, 0, 0], [0, 0, 17.0]],
521
- [1.2, [ -5, 0, 0], [0, 0, 13.0]]
522
- ]],
523
- ["LeftArm", [
524
- [0.0, [ 15, -30, 0]],
525
- [0.15, [ 15, -90, 0]],
526
- [0.3, [ 15, -150, 0]],
527
- [0.6, [ 15, -150, 0]],
528
- [0.9, [ 15, -150, 0]],
529
- [1.05, [ 15, -90, 0]],
530
- [1.2, [ 15, -30, 0]]
531
- ]],
532
- ["LeftForearm", [
533
- [0.0, [ 30, 0, 0]],
534
- [0.3, [ 10, 0, 0]],
535
- [0.6, [ 10, 0, 0]],
536
- [0.9, [ 10, 0, 0]],
537
- [1.2, [ 30, 0, 0]]
538
- ]],
539
- ["RightArm", [
540
- [0.0, [ 15, 30, 0]],
541
- [0.15, [ 15, 90, 0]],
542
- [0.3, [ 15, 150, 0]],
543
- [0.6, [ 15, 150, 0]],
544
- [0.9, [ 15, 150, 0]],
545
- [1.05, [ 15, 90, 0]],
546
- [1.2, [ 15, 30, 0]]
547
- ]],
548
- ["RightForearm", [
549
- [0.0, [ 30, 0, 0]],
550
- [0.3, [ 10, 0, 0]],
551
- [0.6, [ 10, 0, 0]],
552
- [0.9, [ 10, 0, 0]],
553
- [1.2, [ 30, 0, 0]]
554
- ]],
555
- ["LeftLeg", [
556
- [0.0, [ 15, 0, 6]],
557
- [0.3, [ 35, 0, 6]],
558
- [0.6, [ 15, 0, 6]],
559
- [0.9, [ 35, 0, 6]],
560
- [1.2, [ 15, 0, 6]]
561
- ]],
562
- ["LeftShin", [
563
- [0.0, [-15, 0, 0]],
564
- [0.3, [-45, 0, 0]],
565
- [0.6, [-15, 0, 0]],
566
- [0.9, [-45, 0, 0]],
567
- [1.2, [-15, 0, 0]]
568
- ]],
569
- ["RightLeg", [
570
- [0.0, [ 15, 0, -6]],
571
- [0.3, [ 35, 0, -6]],
572
- [0.6, [ 15, 0, -6]],
573
- [0.9, [ 35, 0, -6]],
574
- [1.2, [ 15, 0, -6]]
575
- ]],
576
- ["RightShin", [
577
- [0.0, [-15, 0, 0]],
578
- [0.3, [-45, 0, 0]],
579
- [0.6, [-15, 0, 0]],
580
- [0.9, [-45, 0, 0]],
581
- [1.2, [-15, 0, 0]]
582
- ]]
583
- ]]
584
- ];
585
-
586
- armature(animations=robot_anim) {
587
- bone(name="Root", t=[0, 0, 13.0], r=[-5, 0, 0]) {
588
- robot_body();
589
-
590
- bone(name="LeftArm", t=[8.2, 0.5, 0.0], r=[15, -30, 0]) {
591
- arm_upper();
592
- bone(name="LeftForearm", t=[0, 0, -2.8], r=[30, 0, 0]) {
593
- arm_forearm();
594
- }
595
- }
596
-
597
- bone(name="RightArm", t=[-8.2, 0.5, 0.0], r=[15, 30, 0]) {
598
- arm_upper();
599
- bone(name="RightForearm", t=[0, 0, -2.8], r=[30, 0, 0]) {
600
- arm_forearm();
601
- }
602
- }
603
-
604
- bone(name="LeftLeg", t=[3.6, 0.0, -7.0], r=[15, 0, 6]) {
605
- upper_leg();
606
- bone(name="LeftShin", t=[0, 0, -3.0], r=[-15, 0, 0]) {
607
- lower_leg();
608
- }
609
- }
610
-
611
- bone(name="RightLeg", t=[-3.6, 0.0, -7.0], r=[15, 0, -6]) {
612
- upper_leg();
613
- bone(name="RightShin", t=[0, 0, -3.0], r=[-15, 0, 0]) {
614
- lower_leg();
615
- }
616
- }
617
- }
618
- }
619
- `,
620
- );
621
-
622
- writeFile(
623
- "assets/models/toy_blocks.scad",
624
- `/* Model Name: toy_blocks */
625
- $fn = 24;
626
- $asa = 45.0;
627
-
628
- module toy_block(col) {
629
- color(col, metalness=0.05, roughness=0.50, $asa=45) {
630
- difference() {
631
- cube([0.22, 0.22, 0.22], center=true);
632
- for (rot = [[0,0,0], [90,0,0], [0,90,0], [-90,0,0], [0,-90,0], [180,0,0]]) {
633
- rotate(rot) translate([0, 0, 0.11]) cube([0.16, 0.16, 0.02], center=true);
634
- }
635
- }
636
- }
637
- color([0.90, 0.90, 0.92], metalness=0.0, roughness=0.35, $asa=45) {
638
- sphere(r=0.06);
639
- }
640
- }
641
-
642
- module toy_blocks() {
643
- translate([0, 0, 0.11])
644
- rotate([0, 0, 12])
645
- toy_block([0.85, 0.22, 0.25]);
646
-
647
- translate([0.24, -0.04, 0.11])
648
- rotate([0, 0, -15])
649
- toy_block([0.12, 0.65, 0.85]);
650
-
651
- translate([0.10, -0.02, 0.33])
652
- rotate([0, 0, 6])
653
- toy_block([0.92, 0.75, 0.15]);
654
- }
655
-
656
- toy_blocks();
657
- `,
658
- );
659
-
660
- // -------------------------------------------------------------------------
661
- // 2B. PARTICLE SVG TEXTURES (Stand-alone vector assets)
662
- // -------------------------------------------------------------------------
663
-
664
- writeFile(
665
- "assets/particles/confetti_star.svg",
666
- `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
667
- <polygon points="32,2 41,21 62,24 47,40 50,61 32,51 14,61 17,40 2,24 23,21" fill="#FFFFFF"/>
668
- </svg>`,
669
- );
670
-
671
- writeFile(
672
- "assets/particles/confetti_ribbon.svg",
673
- `<svg xmlns="http://www.w3.org/2000/svg" width="48" height="24" viewBox="0 0 48 24">
674
- <rect x="2" y="3" width="44" height="18" rx="7" ry="7" fill="#FFFFFF"/>
675
- </svg>`,
676
- );
677
-
678
- writeFile(
679
- "assets/particles/sparkle.svg",
680
- `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
681
- <path d="M32,0 Q32,32 64,32 Q32,32 32,64 Q32,32 0,32 Q32,32 32,0 Z" fill="#FFFFFF"/>
682
- </svg>`,
683
- );
684
-
685
- // -------------------------------------------------------------------------
686
- // 3. GDSCRIPT LOGIC
687
- // -------------------------------------------------------------------------
688
-
689
- writeFile(
690
- "scripts/sound_manager.gd",
691
- `class_name SoundManager
692
- extends Node
693
-
694
- var audio_players: Array[AudioStreamPlayer] = []
695
- var max_polyphony: int = 8
696
- var current_player: int = 0
697
- var is_muted: bool = false
698
-
699
- var chime_streams: Array[AudioStreamWAV] = []
700
- var chalk_stream: AudioStreamWAV
701
- var fanfare_stream: AudioStreamWAV
702
- var pop_stream: AudioStreamWAV
703
- var chatter_streams: Array[AudioStreamWAV] = []
704
-
705
- func _ready() -> void:
706
- for i in range(max_polyphony):
707
- var player = AudioStreamPlayer.new()
708
- add_child(player)
709
- audio_players.append(player)
710
- _generate_audio_assets()
711
-
712
- func _generate_audio_assets() -> void:
713
- var pentatonic_notes = [523.25, 587.33, 659.25, 783.99, 880.00, 1046.50, 1174.66, 1318.51]
714
- for freq in pentatonic_notes:
715
- chime_streams.append(_synthesize_bell(freq, 0.55))
716
-
717
- chalk_stream = _synthesize_chalk_stroke()
718
- fanfare_stream = _synthesize_fanfare()
719
- pop_stream = _synthesize_pop()
720
-
721
- for pitch in [620.0, 780.0, 950.0, 1100.0]:
722
- chatter_streams.append(_synthesize_robot_blip(pitch))
723
-
724
- func _play_stream(stream: AudioStreamWAV, volume_db: float = 0.0) -> void:
725
- if is_muted or stream == null:
726
- return
727
- var player = audio_players[current_player]
728
- current_player = (current_player + 1) % max_polyphony
729
- player.stream = stream
730
- player.volume_db = volume_db
731
- player.play()
732
-
733
- func play_chime(index: int) -> void:
734
- if chime_streams.is_empty():
735
- return
736
- var idx = clamp(index, 0, chime_streams.size() - 1)
737
- _play_stream(chime_streams[idx], -3.0)
738
-
739
- func play_chalk() -> void:
740
- _play_stream(chalk_stream, -14.0)
741
-
742
- func play_fanfare() -> void:
743
- _play_stream(fanfare_stream, 0.0)
744
-
745
- func play_pop() -> void:
746
- _play_stream(pop_stream, -4.0)
747
-
748
- func play_robot_voice() -> void:
749
- if chatter_streams.is_empty():
750
- return
751
- var idx = randi() % chatter_streams.size()
752
- _play_stream(chatter_streams[idx], -6.0)
753
-
754
- func toggle_mute() -> bool:
755
- is_muted = !is_muted
756
- return is_muted
757
-
758
- func _synthesize_bell(freq: float, duration: float) -> AudioStreamWAV:
759
- var sample_rate = 22050
760
- var num_samples = int(duration * sample_rate)
761
- var samples = PackedFloat32Array()
762
- samples.resize(num_samples)
763
- var two_pi = TAU
764
-
765
- for i in range(num_samples):
766
- var t = float(i) / float(sample_rate)
767
- var env = exp(-t * 5.5)
768
- var s1 = sin(two_pi * freq * t)
769
- var s2 = 0.35 * sin(two_pi * (freq * 2.0) * t)
770
- var s3 = 0.15 * sin(two_pi * (freq * 3.0) * t)
771
- samples[i] = (s1 + s2 + s3) * env * 0.75
772
-
773
- return _pack_wav(samples, sample_rate)
774
-
775
- func _synthesize_pop() -> AudioStreamWAV:
776
- var sample_rate = 22050
777
- var duration = 0.08
778
- var num_samples = int(duration * sample_rate)
779
- var samples = PackedFloat32Array()
780
- samples.resize(num_samples)
781
-
782
- for i in range(num_samples):
783
- var t = float(i) / float(sample_rate)
784
- var progress = t / duration
785
- var freq = lerp(680.0, 140.0, progress)
786
- var env = 1.0 - progress
787
- samples[i] = sin(TAU * freq * t) * env * 0.8
788
-
789
- return _pack_wav(samples, sample_rate)
790
-
791
- func _synthesize_chalk_stroke() -> AudioStreamWAV:
792
- var sample_rate = 22050
793
- var duration = 0.09
794
- var num_samples = int(duration * sample_rate)
795
- var samples = PackedFloat32Array()
796
- samples.resize(num_samples)
797
-
798
- var noise_prev = 0.0
799
- for i in range(num_samples):
800
- var t = float(i) / float(sample_rate)
801
- var env = sin((t / duration) * PI)
802
- var white = (randf() * 2.0 - 1.0)
803
- noise_prev = lerp(noise_prev, white, 0.35)
804
- samples[i] = noise_prev * env * 0.5
805
-
806
- return _pack_wav(samples, sample_rate)
807
-
808
- func _synthesize_robot_blip(freq: float) -> AudioStreamWAV:
809
- var sample_rate = 22050
810
- var duration = 0.12
811
- var num_samples = int(duration * sample_rate)
812
- var samples = PackedFloat32Array()
813
- samples.resize(num_samples)
814
-
815
- for i in range(num_samples):
816
- var t = float(i) / float(sample_rate)
817
- var env = sin((t / duration) * PI)
818
- var chirp = freq + sin(TAU * 35.0 * t) * 70.0
819
- samples[i] = sin(TAU * chirp * t) * env * 0.65
820
-
821
- return _pack_wav(samples, sample_rate)
822
-
823
- func _synthesize_fanfare() -> AudioStreamWAV:
824
- var sample_rate = 22050
825
- var duration = 1.25
826
- var num_samples = int(duration * sample_rate)
827
- var samples = PackedFloat32Array()
828
- samples.resize(num_samples)
829
-
830
- var arpeggio = [523.25, 659.25, 783.99, 1046.50, 1318.51]
831
- for i in range(num_samples):
832
- var t = float(i) / float(sample_rate)
833
- var note_idx = int(clamp(t / 0.18, 0, arpeggio.size() - 1))
834
- var note_freq = arpeggio[note_idx]
835
- var note_local_t = fmod(t, 0.18)
836
- if note_idx == arpeggio.size() - 1:
837
- note_local_t = t - (0.18 * 4.0)
838
-
839
- var env = exp(-note_local_t * 3.5)
840
- var s = sin(TAU * note_freq * t) + 0.3 * sin(TAU * (note_freq * 2.0) * t)
841
- samples[i] = s * env * 0.7
842
-
843
- return _pack_wav(samples, sample_rate)
844
-
845
- func _pack_wav(samples: PackedFloat32Array, sample_rate: int) -> AudioStreamWAV:
846
- var wav = AudioStreamWAV.new()
847
- wav.format = AudioStreamWAV.FORMAT_16_BITS
848
- wav.mix_rate = sample_rate
849
- wav.stereo = false
850
- var byte_data = PackedByteArray()
851
- byte_data.resize(samples.size() * 2)
852
-
853
- for i in range(samples.size()):
854
- var val = clampf(samples[i], -1.0, 1.0)
855
- var s16 = int(val * 32767.0)
856
- byte_data.encode_s16(i * 2, s16)
857
-
858
- wav.data = byte_data
859
- return wav
860
- `,
861
- );
862
-
863
- writeFile(
864
- "scripts/digit_data.gd",
865
- `class_name DigitData
866
- extends RefCounted
867
-
868
- static func get_digit_info(digit: int) -> Dictionary:
869
- match digit:
870
- 0:
871
- return {
872
- "digit": 0,
873
- "name": "Zero",
874
- "rhyme": "Around and around and around we go,\\nwhen we get home we have a zero!",
875
- "tip": "Start at the upper right! Curve up and left all the way around, and join back at the start!",
876
- "strokes": [
877
- [
878
- Vector2(0.68, 0.26),
879
- Vector2(0.50, 0.16),
880
- Vector2(0.32, 0.22),
881
- Vector2(0.24, 0.40),
882
- Vector2(0.24, 0.60),
883
- Vector2(0.32, 0.78),
884
- Vector2(0.50, 0.84),
885
- Vector2(0.68, 0.78),
886
- Vector2(0.76, 0.60),
887
- Vector2(0.76, 0.40),
888
- Vector2(0.68, 0.26)
889
- ]
890
- ]
891
- }
892
- 1:
893
- return {
894
- "digit": 1,
895
- "name": "One",
896
- "rhyme": "Number one is like a stick,\\na straight line down that's very quick!",
897
- "tip": "Start at the top and slide straight down!",
898
- "strokes": [
899
- [
900
- Vector2(0.40, 0.28),
901
- Vector2(0.50, 0.16),
902
- Vector2(0.50, 0.40),
903
- Vector2(0.50, 0.64),
904
- Vector2(0.50, 0.84)
905
- ]
906
- ]
907
- }
908
- 2:
909
- return {
910
- "digit": 2,
911
- "name": "Two",
912
- "rhyme": "Around and back on the railroad track,\\ntwo, two, two!",
913
- "tip": "Curve around the top, slide down, and across!",
914
- "strokes": [
915
- [
916
- Vector2(0.30, 0.28),
917
- Vector2(0.45, 0.16),
918
- Vector2(0.66, 0.20),
919
- Vector2(0.70, 0.35),
920
- Vector2(0.55, 0.55),
921
- Vector2(0.32, 0.84),
922
- Vector2(0.52, 0.84),
923
- Vector2(0.72, 0.84)
924
- ]
925
- ]
926
- }
927
- 3:
928
- return {
929
- "digit": 3,
930
- "name": "Three",
931
- "rhyme": "Around the tree and around the tree,\\nthat's the way we make a three!",
932
- "tip": "Make two bouncy hops to the right!",
933
- "strokes": [
934
- [
935
- Vector2(0.32, 0.22),
936
- Vector2(0.50, 0.16),
937
- Vector2(0.68, 0.24),
938
- Vector2(0.64, 0.42),
939
- Vector2(0.48, 0.48),
940
- Vector2(0.66, 0.58),
941
- Vector2(0.70, 0.74),
942
- Vector2(0.52, 0.84),
943
- Vector2(0.32, 0.78)
944
- ]
945
- ]
946
- }
947
- 4:
948
- return {
949
- "digit": 4,
950
- "name": "Four",
951
- "rhyme": "Down and over, down once more,\\nthat's the way we make a four!",
952
- "tip": "Two strokes! First an L, then a straight stick!",
953
- "strokes": [
954
- [
955
- Vector2(0.64, 0.16),
956
- Vector2(0.45, 0.42),
957
- Vector2(0.28, 0.64),
958
- Vector2(0.50, 0.64),
959
- Vector2(0.74, 0.64)
960
- ],
961
- [
962
- Vector2(0.64, 0.38),
963
- Vector2(0.64, 0.64),
964
- Vector2(0.64, 0.84)
965
- ]
966
- ]
967
- }
968
- 5:
969
- return {
970
- "digit": 5,
971
- "name": "Five",
972
- "rhyme": "Straight line down, around we go,\\nput on a hat for number five, oh!",
973
- "tip": "Down the neck, around the belly, then the hat!",
974
- "strokes": [
975
- [
976
- Vector2(0.38, 0.20),
977
- Vector2(0.36, 0.44),
978
- Vector2(0.56, 0.44),
979
- Vector2(0.72, 0.56),
980
- Vector2(0.70, 0.74),
981
- Vector2(0.54, 0.84),
982
- Vector2(0.34, 0.80)
983
- ],
984
- [
985
- Vector2(0.38, 0.20),
986
- Vector2(0.54, 0.20),
987
- Vector2(0.70, 0.20)
988
- ]
989
- ]
990
- }
991
- 6:
992
- return {
993
- "digit": 6,
994
- "name": "Six",
995
- "rhyme": "Down to a loop,\\na six rolls in a hoop!",
996
- "tip": "Slide down and roll inside a cozy loop!",
997
- "strokes": [
998
- [
999
- Vector2(0.64, 0.16),
1000
- Vector2(0.42, 0.30),
1001
- Vector2(0.28, 0.54),
1002
- Vector2(0.32, 0.76),
1003
- Vector2(0.50, 0.84),
1004
- Vector2(0.70, 0.74),
1005
- Vector2(0.70, 0.56),
1006
- Vector2(0.50, 0.50),
1007
- Vector2(0.30, 0.60)
1008
- ]
1009
- ]
1010
- }
1011
- 7:
1012
- return {
1013
- "digit": 7,
1014
- "name": "Seven",
1015
- "rhyme": "Across the sky and down from heaven,\\nadd a belt to make a seven!",
1016
- "tip": "Two strokes! Slide across, slant down, and draw a belt across the middle!",
1017
- "strokes": [
1018
- [
1019
- Vector2(0.28, 0.18),
1020
- Vector2(0.50, 0.18),
1021
- Vector2(0.74, 0.18),
1022
- Vector2(0.60, 0.42),
1023
- Vector2(0.48, 0.64),
1024
- Vector2(0.38, 0.84)
1025
- ],
1026
- [
1027
- Vector2(0.42, 0.52),
1028
- Vector2(0.66, 0.52)
1029
- ]
1030
- ]
1031
- }
1032
- 8:
1033
- return {
1034
- "digit": 8,
1035
- "name": "Eight",
1036
- "rhyme": "Make an S and do not wait,\\nclimb back up to make an eight!",
1037
- "tip": "Start at the upper right! Curve up and left, cross down like an S, loop the bottom, and climb back home!",
1038
- "strokes": [
1039
- [
1040
- Vector2(0.64, 0.22),
1041
- Vector2(0.50, 0.16),
1042
- Vector2(0.34, 0.24),
1043
- Vector2(0.36, 0.38),
1044
- Vector2(0.50, 0.48),
1045
- Vector2(0.64, 0.60),
1046
- Vector2(0.66, 0.76),
1047
- Vector2(0.50, 0.84),
1048
- Vector2(0.34, 0.78),
1049
- Vector2(0.34, 0.62),
1050
- Vector2(0.50, 0.48),
1051
- Vector2(0.64, 0.22)
1052
- ]
1053
- ]
1054
- }
1055
- 9:
1056
- return {
1057
- "digit": 9,
1058
- "name": "Nine",
1059
- "rhyme": "Make a loop and come back round,\\nslide down and curl along the ground!",
1060
- "tip": "Start at the upper right! Loop up and around, slide straight down, and curl the tail to the left!",
1061
- "strokes": [
1062
- [
1063
- Vector2(0.66, 0.24),
1064
- Vector2(0.50, 0.16),
1065
- Vector2(0.34, 0.26),
1066
- Vector2(0.34, 0.42),
1067
- Vector2(0.50, 0.50),
1068
- Vector2(0.66, 0.42),
1069
- Vector2(0.66, 0.24),
1070
- Vector2(0.66, 0.52),
1071
- Vector2(0.62, 0.72),
1072
- Vector2(0.50, 0.84),
1073
- Vector2(0.36, 0.84),
1074
- Vector2(0.24, 0.84)
1075
- ]
1076
- ]
1077
- }
1078
- _:
1079
- return get_digit_info(0)
1080
- `,
1081
- );
1082
-
1083
- writeFile(
1084
- "scripts/teacher_robot.gd",
1085
- `class_name TeacherRobot
1086
- extends Node3D
1087
-
1088
- signal speech_requested(text: String)
1089
-
1090
- @export var look_target_node: Node3D
1091
- var anim_player: AnimationPlayer = null
1092
-
1093
- var base_rotation_y: float = deg_to_rad(215.0)
1094
- var target_turn_y: float = 0.0
1095
- var base_position_y: float = 0.08
1096
- var idle_timer: float = 0.0
1097
- var is_praising: bool = false
1098
- var is_grounded: bool = false
1099
-
1100
- func _ready() -> void:
1101
- rotation.y = base_rotation_y
1102
- position.y = base_position_y
1103
- _find_animation_player(self)
1104
- if anim_player:
1105
- anim_player.animation_finished.connect(_on_anim_finished)
1106
- play_idle()
1107
- _ground_to_floor()
1108
-
1109
- func _ground_to_floor() -> void:
1110
- await get_tree().process_frame
1111
-
1112
- var min_y = INF
1113
- var visual_nodes = find_children("*", "VisualInstance3D", true)
1114
- for node in visual_nodes:
1115
- if node is VisualInstance3D:
1116
- var aabb = node.get_aabb()
1117
- if aabb.size.length_squared() < 0.0001:
1118
- continue
1119
- var trans = node.global_transform
1120
- for i in range(8):
1121
- var corner = aabb.get_endpoint(i)
1122
- var world_corner = trans * corner
1123
- min_y = minf(min_y, world_corner.y)
1124
-
1125
- if min_y < INF and absf(min_y) > 0.001:
1126
- position.y -= min_y
1127
- elif min_y == INF:
1128
- position.y = 0.08
1129
-
1130
- base_position_y = position.y
1131
- is_grounded = true
1132
-
1133
- func _find_animation_player(node: Node) -> void:
1134
- if node is AnimationPlayer:
1135
- anim_player = node
1136
- return
1137
- for child in node.get_children():
1138
- _find_animation_player(child)
1139
- if anim_player != null:
1140
- return
1141
-
1142
- func play_idle() -> void:
1143
- is_praising = false
1144
- target_turn_y = 0.0
1145
- if anim_player and anim_player.has_animation("Idle"):
1146
- var anim = anim_player.get_animation("Idle")
1147
- if anim:
1148
- anim.loop_mode = Animation.LOOP_LINEAR
1149
- anim_player.play("Idle")
1150
-
1151
- func play_praise() -> void:
1152
- is_praising = true
1153
- target_turn_y = deg_to_rad(-15.0)
1154
- if anim_player and anim_player.has_animation("Praise"):
1155
- anim_player.play("Praise")
1156
- else:
1157
- var tween = create_tween()
1158
- tween.tween_property(self, "position:y", base_position_y + 0.25, 0.25).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
1159
- tween.tween_property(self, "position:y", base_position_y, 0.25).set_trans(Tween.TRANS_BOUNCE).set_ease(Tween.EASE_OUT)
1160
- tween.tween_callback(func(): is_praising = false)
1161
-
1162
- func _on_anim_finished(anim_name: StringName) -> void:
1163
- if anim_name == "Praise":
1164
- play_idle()
1165
- elif anim_name == "Idle" and not is_praising:
1166
- if anim_player and anim_player.has_animation("Idle"):
1167
- anim_player.play("Idle")
1168
-
1169
- func _process(delta: float) -> void:
1170
- idle_timer += delta
1171
-
1172
- if not is_praising and is_grounded:
1173
- position.y = lerp(position.y, base_position_y, delta * 8.0)
1174
-
1175
- rotation.y = lerp_angle(rotation.y, base_rotation_y + target_turn_y, delta * 4.0)
1176
-
1177
- func look_at_canvas(turn: bool) -> void:
1178
- if is_praising:
1179
- return
1180
- target_turn_y = deg_to_rad(15.0) if turn else 0.0
1181
- `,
1182
- );
1183
-
1184
- writeFile(
1185
- "scripts/writing_canvas.gd",
1186
- `class_name WritingCanvas
1187
- extends Control
1188
-
1189
- signal checkpoint_hit(pos: Vector2, index: int)
1190
- signal digit_completed(stars: int)
1191
- signal stroke_started()
1192
- signal stroke_finished()
1193
- signal demo_finished()
1194
-
1195
- var hit_radius: float = 46.0
1196
- var line_width: float = 18.0
1197
- var guide_visible: bool = true
1198
-
1199
- var digit_data: Dictionary = {}
1200
- var strokes_target: Array = []
1201
- var active_stroke_idx: int = 0
1202
- var active_checkpoint_idx: int = 0
1203
- var completed_checkpoints: Array = []
1204
-
1205
- var drawn_strokes: Array = []
1206
- var current_stroke: PackedVector2Array = PackedVector2Array()
1207
- var is_drawing: bool = false
1208
- var brush_color: Color = Color(0.2, 0.85, 1.0)
1209
-
1210
- var is_demo_playing: bool = false
1211
- var demo_timer: float = 0.0
1212
- var demo_stylus_pos: Vector2 = Vector2.ZERO
1213
-
1214
- var pulse_time: float = 0.0
1215
- var recent_hit_anim: float = 0.0
1216
-
1217
- func _ready() -> void:
1218
- set_process(true)
1219
- queue_redraw()
1220
-
1221
- func load_digit(data: Dictionary) -> void:
1222
- digit_data = data
1223
- strokes_target = data.get("strokes", [])
1224
- reset_canvas()
1225
-
1226
- func reset_canvas() -> void:
1227
- drawn_strokes.clear()
1228
- current_stroke.clear()
1229
- completed_checkpoints.clear()
1230
- active_stroke_idx = 0
1231
- active_checkpoint_idx = 0
1232
- is_drawing = false
1233
- is_demo_playing = false
1234
- queue_redraw()
1235
-
1236
- func undo_last_stroke() -> void:
1237
- if drawn_strokes.size() > 0:
1238
- drawn_strokes.pop_back()
1239
- if active_stroke_idx > 0:
1240
- active_stroke_idx -= 1
1241
- active_checkpoint_idx = 0
1242
- _recompute_completed_checkpoints()
1243
- else:
1244
- completed_checkpoints.clear()
1245
- active_checkpoint_idx = 0
1246
- queue_redraw()
1247
-
1248
- func _recompute_completed_checkpoints() -> void:
1249
- completed_checkpoints.clear()
1250
- for s_idx in range(active_stroke_idx):
1251
- if s_idx < strokes_target.size():
1252
- var pts = strokes_target[s_idx]
1253
- for pt in pts:
1254
- completed_checkpoints.append(pt * size)
1255
-
1256
- func start_demo() -> void:
1257
- reset_canvas()
1258
- is_demo_playing = true
1259
- demo_timer = 0.0
1260
- queue_redraw()
1261
-
1262
- func set_brush_color(col: Color) -> void:
1263
- brush_color = col
1264
- queue_redraw()
1265
-
1266
- func toggle_guides() -> bool:
1267
- guide_visible = !guide_visible
1268
- queue_redraw()
1269
- return guide_visible
1270
-
1271
- func _gui_input(event: InputEvent) -> void:
1272
- if is_demo_playing:
1273
- return
1274
-
1275
- if event is InputEventMouseButton:
1276
- if event.button_index == MOUSE_BUTTON_LEFT:
1277
- if event.pressed:
1278
- _start_drawing(event.position)
1279
- else:
1280
- _finish_drawing()
1281
- elif event is InputEventMouseMotion and is_drawing:
1282
- _add_drawing_point(event.position)
1283
- elif event is InputEventScreenTouch:
1284
- if event.pressed:
1285
- _start_drawing(event.position)
1286
- else:
1287
- _finish_drawing()
1288
- elif event is InputEventScreenDrag and is_drawing:
1289
- _add_drawing_point(event.position)
1290
-
1291
- func _start_drawing(pos: Vector2) -> void:
1292
- is_drawing = true
1293
- current_stroke = PackedVector2Array([pos])
1294
- _check_checkpoint_proximity(pos)
1295
- stroke_started.emit()
1296
- queue_redraw()
1297
-
1298
- func _add_drawing_point(pos: Vector2) -> void:
1299
- if current_stroke.is_empty():
1300
- current_stroke.append(pos)
1301
- else:
1302
- var last_pt = current_stroke[current_stroke.size() - 1]
1303
- if last_pt.distance_squared_to(pos) >= 16.0:
1304
- current_stroke.append(pos)
1305
- _check_checkpoint_proximity(pos)
1306
- queue_redraw()
1307
-
1308
- func _finish_drawing() -> void:
1309
- if is_drawing and current_stroke.size() > 0:
1310
- drawn_strokes.append(current_stroke)
1311
- current_stroke = PackedVector2Array()
1312
- is_drawing = false
1313
- stroke_finished.emit()
1314
- queue_redraw()
1315
-
1316
- func _check_checkpoint_proximity(pos: Vector2) -> void:
1317
- if active_stroke_idx >= strokes_target.size():
1318
- return
1319
-
1320
- var current_stroke_checkpoints = strokes_target[active_stroke_idx]
1321
- if active_checkpoint_idx >= current_stroke_checkpoints.size():
1322
- return
1323
-
1324
- var target_norm = current_stroke_checkpoints[active_checkpoint_idx]
1325
- var target_px = target_norm * size
1326
-
1327
- if pos.distance_to(target_px) <= hit_radius:
1328
- completed_checkpoints.append(target_px)
1329
- checkpoint_hit.emit(target_px, active_checkpoint_idx)
1330
- recent_hit_anim = 1.0
1331
-
1332
- active_checkpoint_idx += 1
1333
- if active_checkpoint_idx >= current_stroke_checkpoints.size():
1334
- active_stroke_idx += 1
1335
- active_checkpoint_idx = 0
1336
-
1337
- if active_stroke_idx >= strokes_target.size():
1338
- var stars = _calculate_stars()
1339
- digit_completed.emit(stars)
1340
-
1341
- func _calculate_stars() -> int:
1342
- var total_checkpoints = 0
1343
- for stroke in strokes_target:
1344
- total_checkpoints += stroke.size()
1345
- if completed_checkpoints.size() >= total_checkpoints:
1346
- return 3
1347
- elif completed_checkpoints.size() >= int(total_checkpoints * 0.75):
1348
- return 2
1349
- return 1
1350
-
1351
- func _process(delta: float) -> void:
1352
- var needs_redraw = false
1353
-
1354
- if recent_hit_anim > 0.0:
1355
- recent_hit_anim = max(0.0, recent_hit_anim - delta * 3.0)
1356
- needs_redraw = true
1357
-
1358
- if is_demo_playing:
1359
- _process_demo(delta)
1360
- needs_redraw = true
1361
-
1362
- # Only animate pulsing guide circles when guides are visible and work remains
1363
- if guide_visible and active_stroke_idx < strokes_target.size():
1364
- pulse_time += delta
1365
- needs_redraw = true
1366
-
1367
- if needs_redraw:
1368
- queue_redraw()
1369
-
1370
- func _process_demo(delta: float) -> void:
1371
- if strokes_target.is_empty():
1372
- is_demo_playing = false
1373
- return
1374
-
1375
- var current_stroke_pts = strokes_target[active_stroke_idx]
1376
- var total_pts = current_stroke_pts.size()
1377
- demo_timer += delta * 1.8
1378
-
1379
- var step_idx = int(demo_timer)
1380
- var fract = fmod(demo_timer, 1.0)
1381
-
1382
- if step_idx < total_pts - 1:
1383
- var p0 = current_stroke_pts[step_idx] * size
1384
- var p1 = current_stroke_pts[step_idx + 1] * size
1385
- demo_stylus_pos = p0.lerp(p1, fract)
1386
-
1387
- if current_stroke.is_empty():
1388
- current_stroke.append(demo_stylus_pos)
1389
- else:
1390
- current_stroke.append(demo_stylus_pos)
1391
-
1392
- if fract < 0.15 and active_checkpoint_idx == step_idx:
1393
- completed_checkpoints.append(p0)
1394
- checkpoint_hit.emit(p0, step_idx)
1395
- active_checkpoint_idx += 1
1396
- else:
1397
- var last_pt = current_stroke_pts[total_pts - 1] * size
1398
- demo_stylus_pos = last_pt
1399
- completed_checkpoints.append(last_pt)
1400
- checkpoint_hit.emit(last_pt, total_pts - 1)
1401
- drawn_strokes.append(current_stroke)
1402
- current_stroke = PackedVector2Array()
1403
-
1404
- active_stroke_idx += 1
1405
- active_checkpoint_idx = 0
1406
- demo_timer = 0.0
1407
-
1408
- if active_stroke_idx >= strokes_target.size():
1409
- is_demo_playing = false
1410
- demo_finished.emit()
1411
-
1412
- func _draw() -> void:
1413
- var canvas_rect = Rect2(Vector2.ZERO, size)
1414
-
1415
- draw_rect(canvas_rect, Color(0.10, 0.14, 0.18), true)
1416
- var line_color = Color(1.0, 1.0, 1.0, 0.10)
1417
- draw_line(Vector2(0, size.y * 0.16), Vector2(size.x, size.y * 0.16), line_color, 2.0)
1418
- draw_dashed_line(Vector2(0, size.y * 0.50), Vector2(size.x, size.y * 0.50), Color(0.3, 0.7, 1.0, 0.20), 3.0, 10.0)
1419
- draw_line(Vector2(0, size.y * 0.84), Vector2(size.x, size.y * 0.84), line_color, 2.0)
1420
-
1421
- if guide_visible:
1422
- for s_idx in range(strokes_target.size()):
1423
- var pts = strokes_target[s_idx]
1424
- for i in range(pts.size() - 1):
1425
- var p1 = pts[i] * size
1426
- var p2 = pts[i + 1] * size
1427
- var guide_color = Color(0.9, 0.9, 1.0, 0.25)
1428
- if s_idx == active_stroke_idx:
1429
- guide_color = Color(1.0, 0.9, 0.3, 0.50)
1430
- draw_dashed_line(p1, p2, guide_color, 4.0, 8.0)
1431
- var mid = p1.lerp(p2, 0.5)
1432
- var dir = (p2 - p1).normalized()
1433
- var normal = Vector2(-dir.y, dir.x)
1434
- draw_line(mid - dir * 6 + normal * 6, mid, guide_color, 3.0)
1435
- draw_line(mid - dir * 6 - normal * 6, mid, guide_color, 3.0)
1436
-
1437
- for stroke in drawn_strokes:
1438
- _draw_chalk_line(stroke, brush_color)
1439
-
1440
- if current_stroke.size() > 1:
1441
- _draw_chalk_line(current_stroke, brush_color)
1442
-
1443
- if guide_visible:
1444
- var default_font = get_theme_default_font()
1445
- for s_idx in range(strokes_target.size()):
1446
- var pts = strokes_target[s_idx]
1447
- for i in range(pts.size()):
1448
- var pt = pts[i] * size
1449
- var is_completed = completed_checkpoints.has(pt)
1450
- var is_next = (s_idx == active_stroke_idx and i == active_checkpoint_idx)
1451
-
1452
- if is_completed:
1453
- draw_circle(pt, 12.0, Color(0.2, 0.85, 0.4, 0.9))
1454
- draw_circle(pt, 6.0, Color(1.0, 1.0, 1.0, 0.9))
1455
- elif is_next:
1456
- var pulse = 1.0 + sin(pulse_time * 6.0) * 0.18
1457
- draw_circle(pt, hit_radius * 0.42 * pulse, Color(1.0, 0.85, 0.2, 0.35))
1458
- draw_circle(pt, 14.0 * pulse, Color(1.0, 0.85, 0.15))
1459
- draw_circle(pt, 7.0, Color(1.0, 1.0, 1.0))
1460
- if i == 0 and default_font:
1461
- draw_string(default_font, pt + Vector2(-4, -18), str(s_idx + 1), HORIZONTAL_ALIGNMENT_CENTER, -1, 16, Color(1, 1, 0.3))
1462
- else:
1463
- draw_circle(pt, 8.0, Color(1.0, 1.0, 1.0, 0.25))
1464
-
1465
- if is_demo_playing:
1466
- draw_circle(demo_stylus_pos, 16.0, Color(1.0, 0.4, 0.2, 0.7))
1467
- draw_circle(demo_stylus_pos, 8.0, Color(0.2, 0.9, 1.0))
1468
-
1469
- func _draw_chalk_line(points: PackedVector2Array, col: Color) -> void:
1470
- if points.size() < 2:
1471
- return
1472
- draw_polyline(points, Color(col.r, col.g, col.b, 0.35), line_width + 8.0, true)
1473
- draw_polyline(points, col, line_width, true)
1474
- draw_circle(points[0], line_width * 0.5, col)
1475
- draw_circle(points[points.size() - 1], line_width * 0.5, col)
1476
- `,
1477
- );
1478
-
1479
- writeFile(
1480
- "scripts/main_game.gd",
1481
- `class_name MainGame
1482
- extends Node3D
1483
-
1484
- @onready var sound_manager: SoundManager = $SoundManager
1485
- @onready var teacher_robot: TeacherRobot = $World3D/MouseRobot
1486
- @onready var toy_blocks: Node3D = $World3D/ToyBlocks
1487
-
1488
- @onready var writing_canvas: WritingCanvas = $CanvasLayer/UI/MainHBox/BoardContainer/BoardPanel/WritingCanvas
1489
- @onready var speech_label: Label = $CanvasLayer/UI/SpeechBalloon/BalloonMargin/VBox/SpeechText
1490
- @onready var speech_balloon: Control = $CanvasLayer/UI/SpeechBalloon
1491
- @onready var celebration_modal: Control = $CanvasLayer/CelebrationModal
1492
- @onready var digit_title: Label = $CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/DigitTitle
1493
- @onready var star_count_label: Label = $CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/StarDisplay/StarMargin/HBox/StarLabel
1494
- @onready var star_icon: Control = $CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/StarDisplay/StarMargin/HBox/StarIcon
1495
- @onready var star_rating_box: Control = $CanvasLayer/CelebrationModal/ModalPanel/VBox/StarRatingBox
1496
- @onready var digit_btn_container: HBoxContainer = $CanvasLayer/UI/BottomBar/DigitScroll/DigitHBox
1497
- @onready var confetti_stars: CPUParticles2D = $CanvasLayer/ConfettiStars
1498
- @onready var confetti_ribbons: CPUParticles2D = $CanvasLayer/ConfettiRibbons
1499
- @onready var checkpoint_sparkles: CPUParticles2D = $CanvasLayer/CheckpointSparkles
1500
- @onready var tools_bar: HBoxContainer = $CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar
1501
-
1502
- var current_digit: int = 1
1503
- var stars_earned_map: Dictionary = {}
1504
- var total_stars: int = 0
1505
- var current_modal_stars: int = 3
1506
-
1507
- var chalk_colors = [
1508
- {"name": "ColorCyan", "color": Color(0.20, 0.85, 1.00)},
1509
- {"name": "ColorYellow", "color": Color(1.00, 0.88, 0.20)},
1510
- {"name": "ColorPink", "color": Color(1.00, 0.45, 0.70)},
1511
- {"name": "ColorGreen", "color": Color(0.35, 0.95, 0.35)},
1512
- {"name": "ColorWhite", "color": Color(0.96, 0.96, 0.98)}
1513
- ]
1514
- var active_color_index: int = 0
1515
-
1516
- func _ready() -> void:
1517
- teacher_robot.rotation_degrees.y = 215.0
1518
- teacher_robot.position = Vector3(-0.85, 0.08, -0.2)
1519
-
1520
- toy_blocks.rotation_degrees.y = 180.0
1521
- toy_blocks.position = Vector3(-1.15, 0.0, 0.15)
1522
-
1523
- if star_icon:
1524
- star_icon.draw.connect(_on_star_icon_draw)
1525
- if star_rating_box:
1526
- star_rating_box.draw.connect(_on_star_rating_box_draw)
1527
-
1528
- _setup_particle_textures()
1529
- _init_star_map()
1530
- _create_digit_buttons()
1531
- _setup_color_swatches()
1532
- _load_digit(1)
1533
-
1534
- writing_canvas.checkpoint_hit.connect(_on_checkpoint_hit)
1535
- writing_canvas.digit_completed.connect(_on_digit_completed)
1536
- writing_canvas.stroke_started.connect(_on_stroke_started)
1537
- writing_canvas.stroke_finished.connect(_on_stroke_finished)
1538
- writing_canvas.demo_finished.connect(_on_demo_finished)
1539
-
1540
- func _setup_particle_textures() -> void:
1541
- var star_tex = _generate_star_texture(36)
1542
- var ribbon_tex = _generate_ribbon_texture(30, 14)
1543
- var sparkle_tex = _generate_sparkle_texture(32)
1544
-
1545
- if confetti_stars:
1546
- confetti_stars.texture = star_tex
1547
- if confetti_ribbons:
1548
- confetti_ribbons.texture = ribbon_tex
1549
- if checkpoint_sparkles:
1550
- checkpoint_sparkles.texture = sparkle_tex
1551
-
1552
- func _init_star_map() -> void:
1553
- for d in range(10):
1554
- stars_earned_map[d] = 0
1555
-
1556
- func _create_digit_buttons() -> void:
1557
- for child in digit_btn_container.get_children():
1558
- child.queue_free()
1559
-
1560
- var order = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
1561
- for d in order:
1562
- var btn = Button.new()
1563
- btn.text = str(d)
1564
- btn.custom_minimum_size = Vector2(64, 60)
1565
- btn.add_theme_font_size_override("font_size", 28)
1566
- btn.focus_mode = Control.FOCUS_NONE
1567
- btn.pressed.connect(func(): _load_digit(d))
1568
- digit_btn_container.add_child(btn)
1569
-
1570
- func _setup_color_swatches() -> void:
1571
- for i in range(chalk_colors.size()):
1572
- var info = chalk_colors[i]
1573
- var btn = tools_bar.get_node_or_null(info.name) as Button
1574
- if btn:
1575
- btn.text = ""
1576
- btn.custom_minimum_size = Vector2(36, 36)
1577
- var idx = i
1578
- btn.pressed.connect(func(): _on_color_swatch_selected(idx))
1579
- _update_color_swatches_ui()
1580
-
1581
- func _on_color_swatch_selected(idx: int) -> void:
1582
- sound_manager.play_pop()
1583
- active_color_index = idx
1584
- writing_canvas.set_brush_color(chalk_colors[idx].color)
1585
- _update_color_swatches_ui()
1586
-
1587
- func _update_color_swatches_ui() -> void:
1588
- for i in range(chalk_colors.size()):
1589
- var info = chalk_colors[i]
1590
- var btn = tools_bar.get_node_or_null(info.name) as Button
1591
- if not btn:
1592
- continue
1593
- var is_active = (i == active_color_index)
1594
- var style = StyleBoxFlat.new()
1595
- style.set_corner_radius_all(18)
1596
- style.bg_color = info.color
1597
- if is_active:
1598
- style.border_width_left = 3
1599
- style.border_width_top = 3
1600
- style.border_width_right = 3
1601
- style.border_width_bottom = 3
1602
- style.border_color = Color.WHITE
1603
- style.shadow_size = 4
1604
- style.shadow_color = Color(1, 1, 1, 0.4)
1605
- else:
1606
- style.border_width_left = 1
1607
- style.border_width_top = 1
1608
- style.border_width_right = 1
1609
- style.border_width_bottom = 1
1610
- style.border_color = Color(1, 1, 1, 0.25)
1611
- btn.add_theme_stylebox_override("normal", style)
1612
- btn.add_theme_stylebox_override("hover", style)
1613
- btn.add_theme_stylebox_override("pressed", style)
1614
-
1615
- func _load_digit(d: int) -> void:
1616
- current_digit = d
1617
- var data = DigitData.get_digit_info(d)
1618
- writing_canvas.load_digit(data)
1619
-
1620
- digit_title.text = "Trace Number " + str(d) + "!"
1621
- _set_speech(data.rhyme)
1622
- sound_manager.play_pop()
1623
- sound_manager.play_robot_voice()
1624
- _update_digit_buttons_ui()
1625
-
1626
- func _update_digit_buttons_ui() -> void:
1627
- var buttons = digit_btn_container.get_children()
1628
- for btn in buttons:
1629
- if not btn is Button:
1630
- continue
1631
- var digit_val = int(btn.text)
1632
- var is_active = (digit_val == current_digit)
1633
- var stars = stars_earned_map.get(digit_val, 0)
1634
-
1635
- var style = StyleBoxFlat.new()
1636
- style.set_corner_radius_all(12)
1637
- if is_active:
1638
- style.bg_color = Color(0.18, 0.45, 0.75, 0.95)
1639
- style.border_width_left = 3
1640
- style.border_width_top = 3
1641
- style.border_width_right = 3
1642
- style.border_width_bottom = 3
1643
- style.border_color = Color(1.0, 0.85, 0.25, 1.0)
1644
- btn.add_theme_color_override("font_color", Color(1.0, 0.95, 0.35))
1645
- elif stars > 0:
1646
- style.bg_color = Color(0.14, 0.24, 0.22, 0.90)
1647
- style.border_width_left = 2
1648
- style.border_width_top = 2
1649
- style.border_width_right = 2
1650
- style.border_width_bottom = 2
1651
- style.border_color = Color(0.25, 0.70, 0.45, 0.7)
1652
- btn.add_theme_color_override("font_color", Color(0.65, 0.95, 0.75))
1653
- else:
1654
- style.bg_color = Color(0.16, 0.20, 0.26, 0.90)
1655
- style.border_width_left = 2
1656
- style.border_width_top = 2
1657
- style.border_width_right = 2
1658
- style.border_width_bottom = 2
1659
- style.border_color = Color(0.28, 0.34, 0.42, 0.7)
1660
- btn.add_theme_color_override("font_color", Color(0.85, 0.90, 0.95))
1661
-
1662
- btn.add_theme_stylebox_override("normal", style)
1663
- btn.add_theme_stylebox_override("hover", style)
1664
- btn.add_theme_stylebox_override("pressed", style)
1665
-
1666
- func _set_speech(text: String) -> void:
1667
- speech_label.text = text
1668
- speech_balloon.visible = true
1669
- speech_balloon.scale = Vector2(0.85, 0.85)
1670
- var tween = create_tween()
1671
- tween.tween_property(speech_balloon, "scale", Vector2.ONE, 0.2).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
1672
-
1673
- func _on_stroke_started() -> void:
1674
- sound_manager.play_chalk()
1675
- teacher_robot.look_at_canvas(true)
1676
-
1677
- func _on_stroke_finished() -> void:
1678
- teacher_robot.look_at_canvas(false)
1679
-
1680
- func _on_checkpoint_hit(pos: Vector2, index: int) -> void:
1681
- sound_manager.play_chime(index)
1682
- _spawn_checkpoint_sparkles(pos)
1683
-
1684
- func _spawn_checkpoint_sparkles(canvas_local_pos: Vector2) -> void:
1685
- if not checkpoint_sparkles:
1686
- return
1687
- checkpoint_sparkles.global_position = writing_canvas.global_position + canvas_local_pos
1688
- checkpoint_sparkles.restart()
1689
- checkpoint_sparkles.emitting = true
1690
-
1691
- func _on_demo_finished() -> void:
1692
- var tween = create_tween()
1693
- tween.tween_interval(1.5)
1694
- tween.tween_callback(func():
1695
- if not writing_canvas.is_demo_playing:
1696
- writing_canvas.reset_canvas()
1697
- )
1698
-
1699
- func _on_digit_completed(stars: int) -> void:
1700
- stars_earned_map[current_digit] = max(stars_earned_map[current_digit], stars)
1701
- _recalculate_stars()
1702
-
1703
- sound_manager.play_fanfare()
1704
- sound_manager.play_robot_voice()
1705
- teacher_robot.play_praise()
1706
-
1707
- _trigger_celebration_confetti()
1708
-
1709
- var data = DigitData.get_digit_info(current_digit)
1710
- _show_celebration(stars, data.rhyme)
1711
-
1712
- func _trigger_celebration_confetti() -> void:
1713
- if confetti_stars:
1714
- confetti_stars.restart()
1715
- confetti_stars.emitting = true
1716
- if confetti_ribbons:
1717
- confetti_ribbons.restart()
1718
- confetti_ribbons.emitting = true
1719
-
1720
- func _recalculate_stars() -> void:
1721
- total_stars = 0
1722
- for d in range(10):
1723
- total_stars += stars_earned_map[d]
1724
- star_count_label.text = str(total_stars) + " / 30"
1725
- _update_digit_buttons_ui()
1726
-
1727
- func _show_celebration(stars: int, rhyme: String) -> void:
1728
- current_modal_stars = stars
1729
- speech_balloon.visible = false
1730
-
1731
- var modal_rhyme_label = $CanvasLayer/CelebrationModal/ModalPanel/VBox/RhymeText
1732
- modal_rhyme_label.text = rhyme
1733
- star_rating_box.queue_redraw()
1734
-
1735
- celebration_modal.visible = true
1736
- celebration_modal.scale = Vector2(0.6, 0.6)
1737
- var tween = create_tween()
1738
- tween.tween_property(celebration_modal, "scale", Vector2.ONE, 0.35).set_trans(Tween.TRANS_ELASTIC).set_ease(Tween.EASE_OUT)
1739
-
1740
- func _on_next_pressed() -> void:
1741
- sound_manager.play_pop()
1742
- celebration_modal.visible = false
1743
- var next_d = (current_digit + 1) % 10
1744
- _load_digit(next_d)
1745
-
1746
- func _on_replay_pressed() -> void:
1747
- sound_manager.play_pop()
1748
- celebration_modal.visible = false
1749
- writing_canvas.reset_canvas()
1750
- teacher_robot.play_idle()
1751
- speech_balloon.visible = true
1752
-
1753
- func _on_demo_btn_pressed() -> void:
1754
- sound_manager.play_pop()
1755
- sound_manager.play_robot_voice()
1756
- var data = DigitData.get_digit_info(current_digit)
1757
- _set_speech("Watch closely! " + data.tip)
1758
- writing_canvas.start_demo()
1759
-
1760
- func _on_clear_btn_pressed() -> void:
1761
- sound_manager.play_pop()
1762
- writing_canvas.reset_canvas()
1763
-
1764
- func _on_undo_btn_pressed() -> void:
1765
- sound_manager.play_pop()
1766
- writing_canvas.undo_last_stroke()
1767
-
1768
- func _on_audio_toggle_pressed() -> void:
1769
- var muted = sound_manager.toggle_mute()
1770
- var btn = $CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/AudioBtn as Button
1771
- if btn:
1772
- btn.text = "Sound: OFF" if muted else "Sound: ON"
1773
-
1774
- func _on_guide_toggle_pressed() -> void:
1775
- var visible_guides = writing_canvas.toggle_guides()
1776
- var btn = $CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/GuideBtn as Button
1777
- if btn:
1778
- btn.text = "Guides: ON" if visible_guides else "Guides: OFF"
1779
-
1780
- # -------------------------------------------------------------
1781
- # Procedural Vector Particle Textures
1782
- # -------------------------------------------------------------
1783
- func _generate_star_texture(size: int = 36) -> ImageTexture:
1784
- var img = Image.create(size, size, false, Image.FORMAT_RGBA8)
1785
- var center = Vector2(size * 0.5, size * 0.5)
1786
- var r_outer = size * 0.46
1787
- var r_inner = size * 0.20
1788
- for y in range(size):
1789
- for x in range(size):
1790
- var pt = Vector2(x + 0.5, y + 0.5)
1791
- var diff = pt - center
1792
- var dist = diff.length()
1793
- if dist > r_outer + 1.0:
1794
- continue
1795
- var angle = atan2(diff.y, diff.x)
1796
- var a_norm = fmod(angle + PI * 0.5 + TAU * 2.0, TAU)
1797
- var arm = fmod(a_norm, TAU / 5.0) / (TAU / 5.0)
1798
- var factor = absf(arm - 0.5) * 2.0
1799
- var r_star = lerp(r_inner, r_outer, factor)
1800
- if dist <= r_star:
1801
- var alpha = clampf((r_star - dist) * 2.0, 0.0, 1.0)
1802
- img.set_pixel(x, y, Color(1.0, 1.0, 1.0, alpha))
1803
- return ImageTexture.create_from_image(img)
1804
-
1805
- func _generate_ribbon_texture(width: int = 30, height: int = 14) -> ImageTexture:
1806
- var img = Image.create(width, height, false, Image.FORMAT_RGBA8)
1807
- var radius = height * 0.40
1808
- for y in range(height):
1809
- for x in range(width):
1810
- var cx = clampf(x + 0.5, radius, width - radius)
1811
- var cy = clampf(y + 0.5, radius, height - radius)
1812
- var dist = Vector2(x + 0.5 - cx, y + 0.5 - cy).length()
1813
- if dist <= radius:
1814
- var alpha = clampf((radius - dist) * 2.0, 0.0, 1.0)
1815
- img.set_pixel(x, y, Color(1.0, 1.0, 1.0, alpha))
1816
- return ImageTexture.create_from_image(img)
1817
-
1818
- func _generate_sparkle_texture(size: int = 32) -> ImageTexture:
1819
- var img = Image.create(size, size, false, Image.FORMAT_RGBA8)
1820
- var center = Vector2(size * 0.5, size * 0.5)
1821
- var half = size * 0.46
1822
- for y in range(size):
1823
- for x in range(size):
1824
- var nx = absf((x + 0.5 - center.x) / half)
1825
- var ny = absf((y + 0.5 - center.y) / half)
1826
- var val = sqrt(nx) + sqrt(ny)
1827
- if val <= 1.0:
1828
- var alpha = clampf((1.0 - val) * 2.5, 0.0, 1.0)
1829
- img.set_pixel(x, y, Color(1.0, 1.0, 1.0, alpha))
1830
- return ImageTexture.create_from_image(img)
1831
-
1832
- # -------------------------------------------------------------
1833
- # Procedural Vector 5-Pointed Star UI Drawing
1834
- # -------------------------------------------------------------
1835
- func _on_star_icon_draw() -> void:
1836
- if not star_icon:
1837
- return
1838
- var radius = minf(star_icon.size.x, star_icon.size.y) * 0.44
1839
- var center = star_icon.size * 0.5
1840
- _draw_5pt_star(star_icon, center, radius, Color(1.0, 0.85, 0.15), Color(1.0, 0.96, 0.55))
1841
-
1842
- func _on_star_rating_box_draw() -> void:
1843
- if not star_rating_box:
1844
- return
1845
- var star_w = 44.0
1846
- var spacing = 18.0
1847
- var total_w = 3.0 * star_w + 2.0 * spacing
1848
- var start_x = (star_rating_box.size.x - total_w) * 0.5 + star_w * 0.5
1849
- var cy = star_rating_box.size.y * 0.5
1850
- for i in range(3):
1851
- var cx = start_x + float(i) * (star_w + spacing)
1852
- var is_filled = (i < current_modal_stars)
1853
- var fill_col = Color(1.0, 0.85, 0.15) if is_filled else Color(0.25, 0.30, 0.38, 0.6)
1854
- var line_col = Color(1.0, 0.95, 0.50) if is_filled else Color(0.40, 0.45, 0.55, 0.6)
1855
- _draw_5pt_star(star_rating_box, Vector2(cx, cy), star_w * 0.5, fill_col, line_col)
1856
-
1857
- static func _draw_5pt_star(ci: CanvasItem, center: Vector2, radius: float, fill_col: Color, line_col: Color) -> void:
1858
- var pts = PackedVector2Array()
1859
- for k in range(10):
1860
- var r = radius if (k % 2 == 0) else (radius * 0.42)
1861
- var angle = (float(k) * PI / 5.0) - (PI * 0.5)
1862
- pts.append(center + Vector2(cos(angle), sin(angle)) * r)
1863
- ci.draw_colored_polygon(pts, fill_col)
1864
- pts.append(pts[0])
1865
- ci.draw_polyline(pts, line_col, 2.2, true)
1866
- `,
1867
- );
1868
-
1869
- // -------------------------------------------------------------------------
1870
- // 4. GODOT 4 MAIN SCENE (.tscn)
1871
- // -------------------------------------------------------------------------
1872
-
1873
- writeFile(
1874
- "scenes/main_game.tscn",
1875
- `[gd_scene format=3 uid="uid://c8yjq01fkl23m"]
1876
-
1877
- [ext_resource type="Script" path="res://scripts/main_game.gd" id="1_main"]
1878
- [ext_resource type="Script" path="res://scripts/sound_manager.gd" id="2_sound"]
1879
- [ext_resource type="PackedScene" path="res://assets/models/mouse_robot.scad" id="3_robot"]
1880
- [ext_resource type="Script" path="res://scripts/teacher_robot.gd" id="4_teacher"]
1881
- [ext_resource type="PackedScene" path="res://assets/models/toy_blocks.scad" id="5_blocks"]
1882
- [ext_resource type="Script" path="res://scripts/writing_canvas.gd" id="6_canvas"]
1883
-
1884
- [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
1885
- sky_top_color = Color(0.28, 0.50, 0.75, 1)
1886
- sky_horizon_color = Color(0.60, 0.70, 0.80, 1)
1887
- ground_bottom_color = Color(0.18, 0.16, 0.14, 1)
1888
- ground_horizon_color = Color(0.55, 0.60, 0.65, 1)
1889
-
1890
- [sub_resource type="Sky" id="Sky_env"]
1891
- sky_material = SubResource("ProceduralSkyMaterial_sky")
1892
-
1893
- [sub_resource type="Environment" id="Environment_main"]
1894
- background_mode = 2
1895
- sky = SubResource("Sky_env")
1896
- ambient_light_source = 2
1897
- ambient_light_color = Color(0.55, 0.60, 0.65, 1)
1898
- tonemap_mode = 3
1899
- tonemap_exposure = 0.6
1900
- glow_enabled = true
1901
- glow_bloom = 0.08
1902
-
1903
- [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
1904
- albedo_color = Color(0.65, 0.52, 0.38, 1)
1905
- roughness = 0.70
1906
-
1907
- [sub_resource type="PlaneMesh" id="PlaneMesh_floor"]
1908
- material = SubResource("StandardMaterial3D_floor")
1909
- size = Vector2(14, 14)
1910
-
1911
- [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_wall"]
1912
- albedo_color = Color(0.48, 0.56, 0.64, 1)
1913
- roughness = 0.85
1914
-
1915
- [sub_resource type="BoxMesh" id="BoxMesh_wall"]
1916
- material = SubResource("StandardMaterial3D_wall")
1917
- size = Vector3(14, 5, 0.2)
1918
-
1919
- [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_header"]
1920
- bg_color = Color(0.12, 0.16, 0.22, 0.95)
1921
- border_width_bottom = 2
1922
- border_color = Color(0.25, 0.35, 0.48, 0.8)
1923
-
1924
- [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_starbox"]
1925
- bg_color = Color(0.18, 0.22, 0.30, 0.9)
1926
- corner_radius_top_left = 14
1927
- corner_radius_top_right = 14
1928
- corner_radius_bottom_right = 14
1929
- corner_radius_bottom_left = 14
1930
- border_width_left = 1
1931
- border_width_top = 1
1932
- border_width_right = 1
1933
- border_width_bottom = 1
1934
- border_color = Color(1, 0.85, 0.25, 0.6)
1935
-
1936
- [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_board"]
1937
- bg_color = Color(0.10, 0.14, 0.18, 1)
1938
- corner_radius_top_left = 14
1939
- corner_radius_top_right = 14
1940
- corner_radius_bottom_right = 14
1941
- corner_radius_bottom_left = 14
1942
- border_width_left = 3
1943
- border_width_top = 3
1944
- border_width_right = 3
1945
- border_width_bottom = 3
1946
- border_color = Color(0.22, 0.28, 0.38, 0.9)
1947
- shadow_size = 10
1948
- shadow_color = Color(0, 0, 0, 0.4)
1949
-
1950
- [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tools"]
1951
- bg_color = Color(0.12, 0.16, 0.22, 0.95)
1952
- corner_radius_top_left = 14
1953
- corner_radius_top_right = 14
1954
- corner_radius_bottom_right = 14
1955
- corner_radius_bottom_left = 14
1956
- border_width_left = 1
1957
- border_width_top = 1
1958
- border_width_right = 1
1959
- border_width_bottom = 1
1960
- border_color = Color(0.25, 0.34, 0.46, 0.8)
1961
- shadow_size = 6
1962
- shadow_color = Color(0, 0, 0, 0.3)
1963
-
1964
- [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_speech"]
1965
- bg_color = Color(0.10, 0.14, 0.20, 0.92)
1966
- corner_radius_top_left = 16
1967
- corner_radius_top_right = 16
1968
- corner_radius_bottom_right = 16
1969
- corner_radius_bottom_left = 16
1970
- border_width_left = 2
1971
- border_width_top = 2
1972
- border_width_right = 2
1973
- border_width_bottom = 2
1974
- border_color = Color(0.20, 0.75, 0.95, 0.60)
1975
- shadow_size = 8
1976
- shadow_color = Color(0, 0, 0, 0.35)
1977
-
1978
- [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bottom"]
1979
- bg_color = Color(0.10, 0.13, 0.18, 0.95)
1980
- border_width_top = 2
1981
- border_color = Color(0.22, 0.28, 0.38, 0.8)
1982
-
1983
- [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_modal"]
1984
- bg_color = Color(0.12, 0.16, 0.22, 0.98)
1985
- corner_radius_top_left = 20
1986
- corner_radius_top_right = 20
1987
- corner_radius_bottom_right = 20
1988
- corner_radius_bottom_left = 20
1989
- border_width_left = 3
1990
- border_width_top = 3
1991
- border_width_right = 3
1992
- border_width_bottom = 3
1993
- border_color = Color(1.0, 0.82, 0.20, 0.95)
1994
- shadow_size = 16
1995
- shadow_color = Color(0, 0, 0, 0.5)
1996
-
1997
- [sub_resource type="Curve" id="Curve_pop"]
1998
- _data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.12, 1), 0.0, 0.0, 0, 0, Vector2(0.75, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
1999
- point_count = 4
2000
-
2001
- [sub_resource type="Gradient" id="Gradient_fade"]
2002
- offsets = PackedFloat32Array(0, 0.15, 0.7, 1)
2003
- colors = PackedColorArray(1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0)
2004
-
2005
- [node name="MainGame" type="Node3D"]
2006
- script = ExtResource("1_main")
2007
-
2008
- [node name="SoundManager" type="Node" parent="."]
2009
- script = ExtResource("2_sound")
2010
-
2011
- [node name="WorldEnvironment" type="WorldEnvironment" parent="."]
2012
- environment = SubResource("Environment_main")
2013
-
2014
- [node name="KeyLight" type="DirectionalLight3D" parent="."]
2015
- transform = Transform3D(0.866, -0.287, 0.409, 0, 0.819, 0.574, -0.5, -0.497, 0.709, 1.2, 4, 3)
2016
- light_color = Color(1, 0.98, 0.92, 1)
2017
- light_energy = 0.5
2018
- shadow_enabled = true
2019
- shadow_opacity = 0.7
2020
- directional_shadow_max_distance = 6.0
2021
- directional_shadow_bias = 0.02
2022
- directional_shadow_normal_bias = 1.0
2023
-
2024
- [node name="FillLight" type="DirectionalLight3D" parent="."]
2025
- transform = Transform3D(0.707, 0, -0.707, -0.353, 0.866, -0.353, 0.612, 0.5, 0.612, -2, 3, 2)
2026
- light_color = Color(0.6, 0.8, 1, 1)
2027
- light_energy = 0.5
2028
- sky_mode = 1
2029
-
2030
- [node name="RimLight" type="DirectionalLight3D" parent="."]
2031
- transform = Transform3D(-0.866, 0, -0.5, 0, 1, 0, 0.5, 0, -0.866, -1, 2, -3)
2032
- light_color = Color(1, 0.7, 0.9, 1)
2033
- light_energy = 0.4
2034
- sky_mode = 1
2035
-
2036
- [node name="Camera3D" type="Camera3D" parent="."]
2037
- transform = Transform3D(1, 0, 0, 0, 0.985, 0.174, 0, -0.174, 0.985, 0.2, 1.22, 2.3)
2038
- current = true
2039
- fov = 55.0
2040
-
2041
- [node name="World3D" type="Node3D" parent="."]
2042
-
2043
- [node name="ClassroomFloor" type="MeshInstance3D" parent="World3D"]
2044
- mesh = SubResource("PlaneMesh_floor")
2045
-
2046
- [node name="ClassroomWall" type="MeshInstance3D" parent="World3D"]
2047
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, -2.4)
2048
- mesh = SubResource("BoxMesh_wall")
2049
-
2050
- [node name="MouseRobot" parent="World3D" instance=ExtResource("3_robot")]
2051
- transform = Transform3D(-0.0508, 0, -0.0356, 0, 0.062, 0, 0.0356, 0, -0.0508, -0.85, 0.08, -0.2)
2052
- script = ExtResource("4_teacher")
2053
-
2054
- [node name="ToyBlocks" parent="World3D" instance=ExtResource("5_blocks")]
2055
- transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -1.15, 0, 0.15)
2056
-
2057
- [node name="CanvasLayer" type="CanvasLayer" parent="."]
2058
-
2059
- [node name="UI" type="Control" parent="CanvasLayer"]
2060
- layout_mode = 3
2061
- anchors_preset = 15
2062
- anchor_right = 1.0
2063
- anchor_bottom = 1.0
2064
- grow_horizontal = 2
2065
- grow_vertical = 2
2066
- mouse_filter = 2
2067
-
2068
- [node name="HeaderBar" type="PanelContainer" parent="CanvasLayer/UI"]
2069
- layout_mode = 1
2070
- anchors_preset = 10
2071
- anchor_right = 1.0
2072
- offset_bottom = 68.0
2073
- grow_horizontal = 2
2074
- theme_override_styles/panel = SubResource("StyleBoxFlat_header")
2075
-
2076
- [node name="HeaderMargin" type="MarginContainer" parent="CanvasLayer/UI/HeaderBar"]
2077
- layout_mode = 2
2078
- theme_override_constants/margin_left = 20
2079
- theme_override_constants/margin_top = 8
2080
- theme_override_constants/margin_right = 20
2081
- theme_override_constants/margin_bottom = 8
2082
-
2083
- [node name="HBox" type="HBoxContainer" parent="CanvasLayer/UI/HeaderBar/HeaderMargin"]
2084
- layout_mode = 2
2085
- theme_override_constants/separation = 16
2086
-
2087
- [node name="AppTitle" type="Label" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox"]
2088
- layout_mode = 2
2089
- theme_override_colors/font_color = Color(1, 0.85, 0.25, 1)
2090
- theme_override_font_sizes/font_size = 22
2091
- text = "Professor Pip's Digit Lab"
2092
-
2093
- [node name="DigitTitle" type="Label" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox"]
2094
- layout_mode = 2
2095
- size_flags_horizontal = 3
2096
- theme_override_colors/font_color = Color(0.3, 0.9, 1, 1)
2097
- theme_override_font_sizes/font_size = 22
2098
- text = "Trace Number 0!"
2099
- horizontal_alignment = 1
2100
-
2101
- [node name="StarDisplay" type="PanelContainer" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox"]
2102
- layout_mode = 2
2103
- theme_override_styles/panel = SubResource("StyleBoxFlat_starbox")
2104
-
2105
- [node name="StarMargin" type="MarginContainer" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/StarDisplay"]
2106
- layout_mode = 2
2107
- theme_override_constants/margin_left = 10
2108
- theme_override_constants/margin_top = 4
2109
- theme_override_constants/margin_right = 12
2110
- theme_override_constants/margin_bottom = 4
2111
-
2112
- [node name="HBox" type="HBoxContainer" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/StarDisplay/StarMargin"]
2113
- layout_mode = 2
2114
- theme_override_constants/separation = 6
2115
-
2116
- [node name="StarIcon" type="Control" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/StarDisplay/StarMargin/HBox"]
2117
- custom_minimum_size = Vector2(24, 24)
2118
- layout_mode = 2
2119
-
2120
- [node name="StarLabel" type="Label" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/StarDisplay/StarMargin/HBox"]
2121
- layout_mode = 2
2122
- theme_override_colors/font_color = Color(1, 0.88, 0.2, 1)
2123
- theme_override_font_sizes/font_size = 18
2124
- text = "0 / 30"
2125
-
2126
- [node name="DemoBtn" type="Button" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox"]
2127
- layout_mode = 2
2128
- theme_override_font_sizes/font_size = 16
2129
- text = "Watch Pip"
2130
-
2131
- [node name="GuideBtn" type="Button" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox"]
2132
- layout_mode = 2
2133
- theme_override_font_sizes/font_size = 16
2134
- text = "Guides: ON"
2135
-
2136
- [node name="AudioBtn" type="Button" parent="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox"]
2137
- layout_mode = 2
2138
- theme_override_font_sizes/font_size = 16
2139
- text = "Sound: ON"
2140
-
2141
- [node name="MainHBox" type="HBoxContainer" parent="CanvasLayer/UI"]
2142
- layout_mode = 1
2143
- anchors_preset = 15
2144
- anchor_right = 1.0
2145
- anchor_bottom = 1.0
2146
- offset_top = 74.0
2147
- offset_bottom = -96.0
2148
- grow_horizontal = 2
2149
- grow_vertical = 2
2150
- mouse_filter = 2
2151
-
2152
- [node name="LeftSpacer" type="Control" parent="CanvasLayer/UI/MainHBox"]
2153
- layout_mode = 2
2154
- size_flags_horizontal = 3
2155
- mouse_filter = 2
2156
-
2157
- [node name="BoardContainer" type="VBoxContainer" parent="CanvasLayer/UI/MainHBox"]
2158
- layout_mode = 2
2159
- size_flags_horizontal = 3
2160
- theme_override_constants/separation = 10
2161
-
2162
- [node name="BoardPanel" type="PanelContainer" parent="CanvasLayer/UI/MainHBox/BoardContainer"]
2163
- layout_mode = 2
2164
- size_flags_vertical = 3
2165
- theme_override_styles/panel = SubResource("StyleBoxFlat_board")
2166
-
2167
- [node name="WritingCanvas" type="Control" parent="CanvasLayer/UI/MainHBox/BoardContainer/BoardPanel"]
2168
- layout_mode = 2
2169
- size_flags_horizontal = 3
2170
- size_flags_vertical = 3
2171
- script = ExtResource("6_canvas")
2172
-
2173
- [node name="ToolsContainer" type="PanelContainer" parent="CanvasLayer/UI/MainHBox/BoardContainer"]
2174
- layout_mode = 2
2175
- theme_override_styles/panel = SubResource("StyleBoxFlat_tools")
2176
-
2177
- [node name="ToolsMargin" type="MarginContainer" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer"]
2178
- layout_mode = 2
2179
- theme_override_constants/margin_left = 14
2180
- theme_override_constants/margin_top = 6
2181
- theme_override_constants/margin_right = 14
2182
- theme_override_constants/margin_bottom = 6
2183
-
2184
- [node name="ToolsBar" type="HBoxContainer" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin"]
2185
- layout_mode = 2
2186
- theme_override_constants/separation = 12
2187
- alignment = 1
2188
-
2189
- [node name="ClearBtn" type="Button" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2190
- custom_minimum_size = Vector2(85, 38)
2191
- layout_mode = 2
2192
- theme_override_font_sizes/font_size = 16
2193
- text = "Clear"
2194
-
2195
- [node name="UndoBtn" type="Button" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2196
- custom_minimum_size = Vector2(85, 38)
2197
- layout_mode = 2
2198
- theme_override_font_sizes/font_size = 16
2199
- text = "Undo"
2200
-
2201
- [node name="ColorLabel" type="Label" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2202
- layout_mode = 2
2203
- theme_override_colors/font_color = Color(0.85, 0.90, 0.96, 1)
2204
- theme_override_font_sizes/font_size = 16
2205
- text = "Chalk:"
2206
-
2207
- [node name="ColorCyan" type="Button" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2208
- custom_minimum_size = Vector2(36, 36)
2209
- layout_mode = 2
2210
- focus_mode = 0
2211
-
2212
- [node name="ColorYellow" type="Button" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2213
- custom_minimum_size = Vector2(36, 36)
2214
- layout_mode = 2
2215
- focus_mode = 0
2216
-
2217
- [node name="ColorPink" type="Button" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2218
- custom_minimum_size = Vector2(36, 36)
2219
- layout_mode = 2
2220
- focus_mode = 0
2221
-
2222
- [node name="ColorGreen" type="Button" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2223
- custom_minimum_size = Vector2(36, 36)
2224
- layout_mode = 2
2225
- focus_mode = 0
2226
-
2227
- [node name="ColorWhite" type="Button" parent="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar"]
2228
- custom_minimum_size = Vector2(36, 36)
2229
- layout_mode = 2
2230
- focus_mode = 0
2231
-
2232
- [node name="RightSpacer" type="Control" parent="CanvasLayer/UI/MainHBox"]
2233
- layout_mode = 2
2234
- size_flags_horizontal = 3
2235
- size_flags_stretch_ratio = 0.1
2236
- mouse_filter = 2
2237
-
2238
- [node name="SpeechBalloon" type="PanelContainer" parent="CanvasLayer/UI"]
2239
- layout_mode = 1
2240
- anchors_preset = 2
2241
- anchor_top = 1.0
2242
- anchor_bottom = 1.0
2243
- offset_left = 24.0
2244
- offset_top = -280.0
2245
- offset_right = 380.0
2246
- offset_bottom = -110.0
2247
- grow_vertical = 0
2248
- theme_override_styles/panel = SubResource("StyleBoxFlat_speech")
2249
-
2250
- [node name="BalloonMargin" type="MarginContainer" parent="CanvasLayer/UI/SpeechBalloon"]
2251
- layout_mode = 2
2252
- theme_override_constants/margin_left = 16
2253
- theme_override_constants/margin_top = 12
2254
- theme_override_constants/margin_right = 16
2255
- theme_override_constants/margin_bottom = 12
2256
-
2257
- [node name="VBox" type="VBoxContainer" parent="CanvasLayer/UI/SpeechBalloon/BalloonMargin"]
2258
- layout_mode = 2
2259
- theme_override_constants/separation = 4
2260
-
2261
- [node name="SpeakerLabel" type="Label" parent="CanvasLayer/UI/SpeechBalloon/BalloonMargin/VBox"]
2262
- layout_mode = 2
2263
- theme_override_colors/font_color = Color(1, 0.85, 0.25, 1)
2264
- theme_override_font_sizes/font_size = 15
2265
- text = "Professor Pip says:"
2266
-
2267
- [node name="SpeechText" type="Label" parent="CanvasLayer/UI/SpeechBalloon/BalloonMargin/VBox"]
2268
- layout_mode = 2
2269
- theme_override_colors/font_color = Color(0.95, 0.98, 1, 1)
2270
- theme_override_font_sizes/font_size = 17
2271
- text = "Around and around and around we go,\\nwhen we get home we have a zero!"
2272
- autowrap_mode = 3
2273
-
2274
- [node name="BottomBar" type="PanelContainer" parent="CanvasLayer/UI"]
2275
- layout_mode = 1
2276
- anchors_preset = 12
2277
- anchor_top = 1.0
2278
- anchor_right = 1.0
2279
- anchor_bottom = 1.0
2280
- offset_top = -88.0
2281
- grow_horizontal = 2
2282
- grow_vertical = 0
2283
- theme_override_styles/panel = SubResource("StyleBoxFlat_bottom")
2284
-
2285
- [node name="DigitScroll" type="ScrollContainer" parent="CanvasLayer/UI/BottomBar"]
2286
- layout_mode = 2
2287
- vertical_scroll_mode = 0
2288
-
2289
- [node name="DigitHBox" type="HBoxContainer" parent="CanvasLayer/UI/BottomBar/DigitScroll"]
2290
- layout_mode = 2
2291
- size_flags_horizontal = 3
2292
- size_flags_vertical = 3
2293
- theme_override_constants/separation = 14
2294
- alignment = 1
2295
-
2296
- [node name="CelebrationModal" type="Control" parent="CanvasLayer"]
2297
- visible = false
2298
- layout_mode = 3
2299
- anchors_preset = 15
2300
- anchor_right = 1.0
2301
- anchor_bottom = 1.0
2302
- grow_horizontal = 2
2303
- grow_vertical = 2
2304
-
2305
- [node name="Backdrop" type="ColorRect" parent="CanvasLayer/CelebrationModal"]
2306
- layout_mode = 1
2307
- anchors_preset = 15
2308
- anchor_right = 1.0
2309
- anchor_bottom = 1.0
2310
- grow_horizontal = 2
2311
- grow_vertical = 2
2312
- color = Color(0.05, 0.08, 0.12, 0.70)
2313
-
2314
- [node name="ModalPanel" type="PanelContainer" parent="CanvasLayer/CelebrationModal"]
2315
- layout_mode = 1
2316
- anchors_preset = 8
2317
- anchor_left = 0.5
2318
- anchor_top = 0.5
2319
- anchor_right = 0.5
2320
- anchor_bottom = 0.5
2321
- offset_left = -250.0
2322
- offset_top = -170.0
2323
- offset_right = 250.0
2324
- offset_bottom = 170.0
2325
- grow_horizontal = 2
2326
- grow_vertical = 2
2327
- theme_override_styles/panel = SubResource("StyleBoxFlat_modal")
2328
-
2329
- [node name="VBox" type="VBoxContainer" parent="CanvasLayer/CelebrationModal/ModalPanel"]
2330
- layout_mode = 2
2331
- theme_override_constants/separation = 14
2332
- alignment = 1
2333
-
2334
- [node name="CongratsTitle" type="Label" parent="CanvasLayer/CelebrationModal/ModalPanel/VBox"]
2335
- layout_mode = 2
2336
- theme_override_colors/font_color = Color(1, 0.85, 0.2, 1)
2337
- theme_override_font_sizes/font_size = 30
2338
- text = "SUPER WRITER!"
2339
- horizontal_alignment = 1
2340
-
2341
- [node name="StarRatingBox" type="Control" parent="CanvasLayer/CelebrationModal/ModalPanel/VBox"]
2342
- custom_minimum_size = Vector2(220, 52)
2343
- layout_mode = 2
2344
-
2345
- [node name="RhymeText" type="Label" parent="CanvasLayer/CelebrationModal/ModalPanel/VBox"]
2346
- layout_mode = 2
2347
- theme_override_font_sizes/font_size = 19
2348
- text = "Great job writing your digit!"
2349
- horizontal_alignment = 1
2350
- autowrap_mode = 3
2351
-
2352
- [node name="BtnHBox" type="HBoxContainer" parent="CanvasLayer/CelebrationModal/ModalPanel/VBox"]
2353
- layout_mode = 2
2354
- theme_override_constants/separation = 20
2355
- alignment = 1
2356
-
2357
- [node name="ReplayBtn" type="Button" parent="CanvasLayer/CelebrationModal/ModalPanel/VBox/BtnHBox"]
2358
- custom_minimum_size = Vector2(130, 46)
2359
- layout_mode = 2
2360
- theme_override_font_sizes/font_size = 17
2361
- text = "Try Again"
2362
-
2363
- [node name="NextBtn" type="Button" parent="CanvasLayer/CelebrationModal/ModalPanel/VBox/BtnHBox"]
2364
- custom_minimum_size = Vector2(160, 46)
2365
- layout_mode = 2
2366
- theme_override_colors/font_color = Color(0.2, 1, 0.4, 1)
2367
- theme_override_font_sizes/font_size = 18
2368
- text = "Next Number >"
2369
-
2370
- [node name="ConfettiStars" type="CPUParticles2D" parent="CanvasLayer"]
2371
- position = Vector2(640, 360)
2372
- emitting = false
2373
- amount = 22
2374
- lifetime = 1.2
2375
- one_shot = true
2376
- explosiveness = 0.85
2377
- emission_shape = 1
2378
- emission_sphere_radius = 50.0
2379
- direction = Vector2(0, -1)
2380
- spread = 180.0
2381
- gravity = Vector2(0, 380)
2382
- initial_velocity_min = 220.0
2383
- initial_velocity_max = 440.0
2384
- angle_min = 0.0
2385
- angle_max = 360.0
2386
- angular_velocity_min = -200.0
2387
- angular_velocity_max = 200.0
2388
- scale_amount_min = 0.7
2389
- scale_amount_max = 1.2
2390
- scale_amount_curve = SubResource("Curve_pop")
2391
- color = Color(1, 0.88, 0.2, 1)
2392
- color_ramp = SubResource("Gradient_fade")
2393
-
2394
- [node name="ConfettiRibbons" type="CPUParticles2D" parent="CanvasLayer"]
2395
- position = Vector2(640, 360)
2396
- emitting = false
2397
- amount = 24
2398
- lifetime = 1.3
2399
- one_shot = true
2400
- explosiveness = 0.88
2401
- emission_shape = 1
2402
- emission_sphere_radius = 60.0
2403
- direction = Vector2(0, -1)
2404
- spread = 180.0
2405
- gravity = Vector2(0, 300)
2406
- initial_velocity_min = 180.0
2407
- initial_velocity_max = 400.0
2408
- angle_min = 0.0
2409
- angle_max = 360.0
2410
- angular_velocity_min = -280.0
2411
- angular_velocity_max = 280.0
2412
- scale_amount_min = 0.7
2413
- scale_amount_max = 1.3
2414
- scale_amount_curve = SubResource("Curve_pop")
2415
- color = Color(0.2, 0.85, 1, 1)
2416
- color_ramp = SubResource("Gradient_fade")
2417
-
2418
- [node name="CheckpointSparkles" type="CPUParticles2D" parent="CanvasLayer"]
2419
- position = Vector2(0, 0)
2420
- emitting = false
2421
- amount = 14
2422
- lifetime = 0.55
2423
- one_shot = true
2424
- explosiveness = 0.92
2425
- emission_shape = 0
2426
- spread = 180.0
2427
- gravity = Vector2(0, 100)
2428
- initial_velocity_min = 80.0
2429
- initial_velocity_max = 180.0
2430
- angle_min = 0.0
2431
- angle_max = 360.0
2432
- angular_velocity_min = -180.0
2433
- angular_velocity_max = 180.0
2434
- scale_amount_min = 0.5
2435
- scale_amount_max = 1.0
2436
- scale_amount_curve = SubResource("Curve_pop")
2437
- color = Color(1, 0.90, 0.35, 1)
2438
- color_ramp = SubResource("Gradient_fade")
2439
-
2440
- [connection signal="pressed" from="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/DemoBtn" to="." method="_on_demo_btn_pressed"]
2441
- [connection signal="pressed" from="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/GuideBtn" to="." method="_on_guide_toggle_pressed"]
2442
- [connection signal="pressed" from="CanvasLayer/UI/HeaderBar/HeaderMargin/HBox/AudioBtn" to="." method="_on_audio_toggle_pressed"]
2443
- [connection signal="pressed" from="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar/ClearBtn" to="." method="_on_clear_btn_pressed"]
2444
- [connection signal="pressed" from="CanvasLayer/UI/MainHBox/BoardContainer/ToolsContainer/ToolsMargin/ToolsBar/UndoBtn" to="." method="_on_undo_btn_pressed"]
2445
- [connection signal="pressed" from="CanvasLayer/CelebrationModal/ModalPanel/VBox/BtnHBox/ReplayBtn" to="." method="_on_replay_pressed"]
2446
- [connection signal="pressed" from="CanvasLayer/CelebrationModal/ModalPanel/VBox/BtnHBox/NextBtn" to="." method="_on_next_pressed"]
2447
- `,
2448
- );
2449
-
2450
- // -------------------------------------------------------------------------
2451
- // 5. PROJECT SETTINGS, GITIGNORE & README
2452
- // -------------------------------------------------------------------------
2453
-
2454
- writeFile(
2455
- "project.godot",
2456
- `; Engine configuration file.
2457
- ; It's best edited using the editor UI and not directly,
2458
- ; but it can be edited here for direct configuration.
2459
-
2460
- config_version=5
2461
-
2462
- [application]
2463
-
2464
- config/name="Robot Digit Writing Lab"
2465
- config/description="Interactive 3D Kids Digit Writing App with Teacher Mouse Robot"
2466
- run/main_scene="res://scenes/main_game.tscn"
2467
- config/features=PackedStringArray("4.3", "GL Compatibility")
2468
-
2469
- [display]
2470
-
2471
- window/size/viewport_width=1280
2472
- window/size/viewport_height=720
2473
- window/stretch/mode="canvas_items"
2474
- window/stretch/aspect="expand"
2475
- window/handheld/orientation=6
2476
-
2477
- [editor_plugins]
2478
-
2479
- enabled=PackedStringArray("res://addons/scad_importer/plugin.cfg")
2480
-
2481
- [rendering]
2482
-
2483
- renderer/rendering_method="gl_compatibility"
2484
- renderer/rendering_method.web="gl_compatibility"
2485
- anti_aliasing/quality/msaa_3d=2
2486
- `,
2487
- );
2488
-
2489
- writeFile(
2490
- ".gitignore",
2491
- `
2492
- .godot/
2493
- *.tmp
2494
- *.log
2495
- `,
2496
- );
2497
-
2498
- writeFile(
2499
- "README.md",
2500
- `# Robot Digit Writing Lab
2501
-
2502
- An educational, interactive 3D digit writing learning app for kids created with Godot 4 and procedural OpenSCAD 3D assets.
2503
-
2504
- ## Features
2505
-
2506
- - **3D Teacher Mouse Robot ("Professor Pip")**:
2507
- - Procedural hierarchical bone armature animation.
2508
- - Idle breathing loop and joyful jumping "Praise" animation when kids complete digits!
2509
- - Fully calibrated anti-glare toy materials and soft directional contact shadows.
2510
- - Reacts dynamically: turns towards the board while tracing, faces the child during celebrations!
2511
-
2512
- - **Optimized Vector Particle System**:
2513
- - Anti-aliased Star, Ribbon, and Sparkle procedural textures.
2514
- - Benchmarked for continuous 60 FPS in GL Compatibility mode.
2515
- - Lightweight celebration bursts and touch feedback with zero frame hitching.
2516
-
2517
- - **Preschool Digit Tracing Curriculum (0 through 9)**:
2518
- - Guided stroke paths with numbered directional arrows and checkpoint beads.
2519
- - Traditional preschool digit rhymes for all digits 0-9.
2520
- - Multi-stroke support for complex digits like 4, 5, and 7.
2521
- - "Watch Pip" Demo Mode: Professor Pip animates a magic stylus along the strokes.
2522
- `,
2523
- );
2524
-
2525
- console.log(
2526
- '\nProject "kids_digit_writing" successfully updated with performance optimizations!',
2527
- );
2528
- console.log("You can now run:");
2529
- console.log(" node kids_digit_writing.js");
2530
- console.log(
2531
- " ~/Documents/projects/external/godot/bin/godot.linuxbsd.editor.x86_64 -e",
2532
- );