scad-gltf 0.2.1 → 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,1774 +0,0 @@
1
- #!/usr/bin/env node
2
- const fs = require("fs");
3
- const path = require("path");
4
-
5
- const PROJECT_DIR = "fruit_fusion_3d";
6
-
7
- function ensureDir(dirPath) {
8
- if (!fs.existsSync(dirPath)) {
9
- fs.mkdirSync(dirPath, { recursive: true });
10
- }
11
- }
12
-
13
- function writeFile(relPath, content) {
14
- const fullPath = path.join(PROJECT_DIR, relPath);
15
- ensureDir(path.dirname(fullPath));
16
- fs.writeFileSync(fullPath, content, "utf8");
17
- console.log(`[Created] ${relPath}`);
18
- }
19
-
20
- console.log(`Setting up project in ./${PROJECT_DIR}...`);
21
-
22
- // ==========================================
23
- // 1. ADDON FILES
24
- // ==========================================
25
-
26
- writeFile(
27
- "addons/scad_importer/plugin.cfg",
28
- `[plugin]
29
-
30
- name="OpenSCAD GLTF Importer"
31
- description="Imports .scad files directly as 3D scenes using scad-gltf"
32
- author="Ilia Grigorev"
33
- version="0.1"
34
- script="scad_plugin.gd"
35
- `,
36
- );
37
-
38
- writeFile(
39
- "addons/scad_importer/scad_plugin.gd",
40
- `@tool
41
- extends EditorPlugin
42
-
43
- var import_plugin
44
-
45
- func _enter_tree():
46
- import_plugin = preload("res://addons/scad_importer/scad_importer.gd").new()
47
- add_scene_format_importer_plugin(import_plugin)
48
-
49
- func _exit_tree():
50
- remove_scene_format_importer_plugin(import_plugin)
51
- import_plugin = null
52
- `,
53
- );
54
-
55
- writeFile(
56
- "addons/scad_importer/scad_importer.gd",
57
- `@tool
58
- extends EditorSceneFormatImporter
59
-
60
- func _get_extensions():
61
- return PackedStringArray(["scad"])
62
-
63
- func _get_import_flags():
64
- return EditorSceneFormatImporter.IMPORT_SCENE
65
-
66
- func _import_scene(path: String, flags: int, options: Dictionary) -> Object:
67
- var global_source = ProjectSettings.globalize_path(path)
68
- var unique_id = str(hash(path))
69
- var temp_glb_path = ProjectSettings.globalize_path("user://scad_cache_" + unique_id + ".glb")
70
-
71
- var args = PackedStringArray()
72
- args.append(global_source)
73
- args.append(temp_glb_path)
74
-
75
- var output = []
76
- print("Importing %s via scad-convert..." % path.get_file())
77
-
78
- var exit_code = -1
79
- if OS.get_name() == "Windows":
80
- var win_args = PackedStringArray(["/c", "scad-convert"])
81
- win_args.append_array(args)
82
- exit_code = OS.execute("cmd.exe", win_args, output, true)
83
- else:
84
- exit_code = OS.execute("scad-convert", args, output, true)
85
-
86
- if exit_code != 0:
87
- print("scad-convert conversion failed for %s. Attempting fallback to local scad-serve..." % path.get_file())
88
- var fallback_success = _try_scad_serve_fallback(global_source, temp_glb_path)
89
-
90
- if not fallback_success:
91
- push_error("Failed to compile SCAD file: %s. Ensure Node.js is installed or scad-serve is running." % path.get_file())
92
- push_error("scad-convert output: ", "\\n".join(output))
93
- return null
94
-
95
- var gltf_doc = GLTFDocument.new()
96
- var gltf_state = GLTFState.new()
97
- var err = gltf_doc.append_from_file(temp_glb_path, gltf_state)
98
-
99
- if FileAccess.file_exists(temp_glb_path):
100
- DirAccess.remove_absolute(temp_glb_path)
101
-
102
- if err != OK:
103
- push_error("Failed to parse the generated GLB for %s." % path.get_file())
104
- return null
105
-
106
- var generated_scene = gltf_doc.generate_scene(gltf_state)
107
- if generated_scene:
108
- generated_scene.name = path.get_file().get_basename()
109
-
110
- return generated_scene
111
-
112
- func _get_relative_path(base: String, target: String) -> String:
113
- var base_parts = base.replace("\\\\", "/").split("/", false)
114
- var target_parts = target.replace("\\\\", "/").split("/", false)
115
-
116
- if OS.get_name() == "Windows":
117
- if base_parts.size() > 0 and target_parts.size() > 0:
118
- if base_parts[0].nocasecmp_to(target_parts[0]) != 0:
119
- return target
120
-
121
- var common_count = 0
122
- var min_len = min(base_parts.size(), target_parts.size())
123
- for i in range(min_len):
124
- if base_parts[i].nocasecmp_to(target_parts[i]) == 0:
125
- common_count += 1
126
- else:
127
- break
128
-
129
- var rel_parts = PackedStringArray()
130
- for i in range(common_count, base_parts.size()):
131
- rel_parts.append("..")
132
-
133
- for i in range(common_count, target_parts.size()):
134
- rel_parts.append(target_parts[i])
135
-
136
- return "/".join(rel_parts)
137
-
138
- func _get_dependencies_recursive(file_path: String, visited: Dictionary) -> void:
139
- if visited.has(file_path):
140
- return
141
-
142
- visited[file_path] = ""
143
-
144
- if not FileAccess.file_exists(file_path):
145
- return
146
-
147
- var file = FileAccess.open(file_path, FileAccess.READ)
148
- if not file:
149
- return
150
-
151
- var content = file.get_as_text()
152
- file.close()
153
-
154
- visited[file_path] = content
155
-
156
- var regex = RegEx.new()
157
- regex.compile("(?:include|use)\\\\s*[<\\"]([^>\\"]+)[>\\"]")
158
-
159
- var base_dir = file_path.get_base_dir()
160
- for result in regex.search_all(content):
161
- var dep_rel_path = result.get_string(1)
162
- var dep_abs_path = base_dir.path_join(dep_rel_path).simplify_path()
163
- _get_dependencies_recursive(dep_abs_path, visited)
164
-
165
- func _get_dependencies(file_path: String) -> Dictionary:
166
- var visited = {}
167
- _get_dependencies_recursive(file_path, visited)
168
- return visited
169
-
170
- func _try_scad_serve_fallback(source_path: String, out_glb_path: String) -> bool:
171
- var deps = _get_dependencies(source_path)
172
- var content = deps.get(source_path, "")
173
- deps.erase(source_path)
174
-
175
- if content == "":
176
- return false
177
-
178
- var additional_files = {}
179
- var base_dir = source_path.get_base_dir()
180
- for dep_path in deps.keys():
181
- var rel_path = _get_relative_path(base_dir, dep_path)
182
- additional_files[rel_path] = deps[dep_path]
183
-
184
- var http = HTTPClient.new()
185
- var err = http.connect_to_host("127.0.0.1", 3000)
186
- if err != OK:
187
- return false
188
-
189
- var max_wait = 500
190
- var wait = 0
191
- while http.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]:
192
- http.poll()
193
- OS.delay_msec(10)
194
- wait += 1
195
- if wait > max_wait:
196
- return false
197
-
198
- if http.get_status() != HTTPClient.STATUS_CONNECTED:
199
- return false
200
-
201
- var headers = PackedStringArray(["Content-Type: application/json"])
202
-
203
- var payload = {
204
- "content": content,
205
- "options": {
206
- "additionalFiles": additional_files
207
- }
208
- }
209
-
210
- var body = JSON.stringify(payload)
211
- err = http.request(HTTPClient.METHOD_POST, "/api/convert", headers, body)
212
- if err != OK:
213
- return false
214
-
215
- max_wait = 6000
216
- wait = 0
217
- while http.get_status() == HTTPClient.STATUS_REQUESTING:
218
- http.poll()
219
- OS.delay_msec(10)
220
- wait += 1
221
- if wait > max_wait:
222
- return false
223
-
224
- if http.has_response() and http.get_response_code() == 200:
225
- var rb = PackedByteArray()
226
- while http.get_status() == HTTPClient.STATUS_BODY:
227
- http.poll()
228
- var chunk = http.read_response_body_chunk()
229
- if chunk.size() == 0:
230
- OS.delay_msec(10)
231
- else:
232
- rb.append_array(chunk)
233
-
234
- if rb.is_empty():
235
- return false
236
-
237
- var out_file = FileAccess.open(out_glb_path, FileAccess.WRITE)
238
- if not out_file:
239
- return false
240
- out_file.store_buffer(rb)
241
- out_file.close()
242
-
243
- print("Successfully compiled %s using scad-serve fallback." % source_path.get_file())
244
- return true
245
-
246
- return false
247
- `,
248
- );
249
-
250
- // ==========================================
251
- // 2. PROCEDURAL 3D ASSETS (.scad)
252
- // ==========================================
253
-
254
- // Container Glass Box with Wooden Border and Danger Marker
255
- writeFile(
256
- "assets/models/container.scad",
257
- `$fn = 28;
258
- $asa = 45;
259
-
260
- box_w = 12.0;
261
- box_d = 4.0;
262
- box_h = 16.0;
263
- wall_t = 0.2;
264
-
265
- // Left, Right & Back Transparent Glass (alpha=0.15 triggers glTF BLEND transparency mode)
266
- color([0.80, 0.93, 1.0], alpha=0.15, roughness=0.04, $asa=45) {
267
- // Back glass plate
268
- translate([0, box_d/2 + wall_t/2, box_h/2])
269
- cube([box_w, wall_t, box_h], center=true);
270
-
271
- // Left glass plate
272
- translate([-box_w/2 - wall_t/2, 0, box_h/2])
273
- cube([wall_t, box_d, box_h], center=true);
274
-
275
- // Right glass plate
276
- translate([box_w/2 + wall_t/2, 0, box_h/2])
277
- cube([wall_t, box_d, box_h], center=true);
278
- }
279
-
280
- // Sturdy Polished Wooden Base
281
- color([0.55, 0.32, 0.16], roughness=0.75) {
282
- translate([0, 0, -0.4])
283
- cube([box_w + 1.6, box_d + 1.6, 0.8], center=true);
284
- }
285
-
286
- // Metallic Corner Columns & Base Trim
287
- color([0.85, 0.88, 0.92], metalness=0.9, roughness=0.2) {
288
- for (sx = [-1, 1]) {
289
- for (sy = [-1, 1]) {
290
- translate([sx * (box_w/2 + wall_t), sy * (box_d/2 + wall_t), box_h/2])
291
- cylinder(r=0.22, h=box_h, center=true);
292
- }
293
- }
294
- // Sleek metallic floor plate
295
- translate([0, 0, 0.05])
296
- cube([box_w, box_d, 0.1], center=true);
297
- }
298
- `,
299
- );
300
-
301
- // Cute Dropper / Aiming Cloud
302
- writeFile(
303
- "assets/models/dropper.scad",
304
- `$fn = 24;
305
- $asa = 45;
306
-
307
- color([0.98, 0.98, 1.0], roughness=0.4) {
308
- translate([0, 0, 0]) sphere(r=0.9);
309
- translate([-0.8, 0, -0.15]) sphere(r=0.65);
310
- translate([0.8, 0, -0.15]) sphere(r=0.65);
311
- translate([-1.4, 0, -0.3]) sphere(r=0.45);
312
- translate([1.4, 0, -0.3]) sphere(r=0.45);
313
- }
314
-
315
- color([1.0, 0.45, 0.55], emissive=[0.8, 0.2, 0.3], emissiveIntensity=0.8, roughness=0.5) {
316
- translate([-0.65, 0.55, -0.2]) sphere(r=0.18);
317
- translate([0.65, 0.55, -0.2]) sphere(r=0.18);
318
- }
319
-
320
- color([1.0, 0.85, 0.1], emissive=[1.0, 0.7, 0.0], emissiveIntensity=1.8, metalness=0.3, roughness=0.3) {
321
- translate([0, 0, -0.95])
322
- cylinder(r1=0.25, r2=0.02, h=0.5, center=true);
323
- }
324
- `,
325
- );
326
-
327
- // Tier 1: Cherry
328
- writeFile(
329
- "assets/models/fruit_1.scad",
330
- `$fn = 26;
331
- $asa = 45;
332
- r = 0.55;
333
-
334
- color([0.85, 0.05, 0.15], roughness=0.15) {
335
- sphere(r=r);
336
- }
337
-
338
- color([0.25, 0.65, 0.15], roughness=0.6) {
339
- translate([0, 0, r * 0.9])
340
- cylinder(r=0.08, h=0.12, center=true);
341
- translate([0.05, 0, r + 0.25])
342
- rotate([0, 15, 0])
343
- cylinder(r=0.04, h=0.5, center=true);
344
- translate([0.18, 0, r + 0.35])
345
- rotate([0, 45, 10])
346
- cube([0.22, 0.1, 0.04], center=true);
347
- }
348
- `,
349
- );
350
-
351
- // Tier 2: Strawberry
352
- writeFile(
353
- "assets/models/fruit_2.scad",
354
- `$fn = 26;
355
- $asa = 45;
356
-
357
- color([0.95, 0.12, 0.28], roughness=0.3) {
358
- scale([1.0, 1.0, 1.25])
359
- sphere(r=0.72);
360
- }
361
-
362
- color([0.18, 0.75, 0.2], roughness=0.5) {
363
- for (i = [0:5]) {
364
- rotate([0, 0, i * 60])
365
- translate([0.35, 0, 0.82])
366
- rotate([0, -25, 0])
367
- cube([0.35, 0.14, 0.05], center=true);
368
- }
369
- translate([0, 0, 0.98])
370
- cylinder(r=0.06, h=0.25, center=true);
371
- }
372
-
373
- color([1.0, 0.9, 0.3], roughness=0.4) {
374
- for (a = [0:5]) {
375
- rotate([0, 0, a * 60 + 30])
376
- translate([0.65, 0, 0.1])
377
- sphere(r=0.05);
378
- rotate([0, 0, a * 60])
379
- translate([0.5, 0, -0.4])
380
- sphere(r=0.045);
381
- }
382
- }
383
- `,
384
- );
385
-
386
- // Tier 3: Grape
387
- writeFile(
388
- "assets/models/fruit_3.scad",
389
- `$fn = 28;
390
- $asa = 45;
391
- r = 0.98;
392
-
393
- color([0.48, 0.12, 0.68], roughness=0.2) {
394
- sphere(r=r);
395
- }
396
-
397
- color([0.3, 0.65, 0.2], roughness=0.6) {
398
- translate([0, 0, r + 0.15])
399
- cylinder(r=0.07, h=0.35, center=true);
400
- translate([0.22, 0, r + 0.15])
401
- rotate([15, -20, 30])
402
- cube([0.35, 0.25, 0.04], center=true);
403
- }
404
- `,
405
- );
406
-
407
- // Tier 4: Orange / Tangerine
408
- writeFile(
409
- "assets/models/fruit_4.scad",
410
- `$fn = 28;
411
- $asa = 45;
412
- r = 1.3;
413
-
414
- color([1.0, 0.48, 0.02], roughness=0.45) {
415
- scale([1.0, 1.0, 0.92])
416
- sphere(r=r);
417
- }
418
-
419
- color([0.2, 0.55, 0.15], roughness=0.6) {
420
- translate([0, 0, r * 0.9])
421
- cylinder(r1=0.15, r2=0.06, h=0.18, center=true);
422
- translate([0.28, 0.1, r * 0.9 + 0.1])
423
- rotate([20, -15, 25])
424
- cube([0.45, 0.22, 0.05], center=true);
425
- }
426
- `,
427
- );
428
-
429
- // Tier 5: Apple
430
- writeFile(
431
- "assets/models/fruit_5.scad",
432
- `$fn = 30;
433
- $asa = 45;
434
- r = 1.65;
435
-
436
- difference() {
437
- color([0.9, 0.08, 0.12], roughness=0.15) {
438
- scale([1.0, 1.0, 0.95])
439
- sphere(r=r);
440
- }
441
- translate([0, 0, r * 0.95])
442
- sphere(r=0.4);
443
- translate([0, 0, -r * 0.95])
444
- sphere(r=0.35);
445
- }
446
-
447
- color([0.35, 0.2, 0.1], roughness=0.8) {
448
- translate([0.05, 0, r * 0.9])
449
- rotate([0, 12, 0])
450
- cylinder(r=0.07, h=0.55, center=true);
451
- }
452
-
453
- color([0.15, 0.7, 0.2], roughness=0.4) {
454
- translate([0.35, 0, r * 0.95])
455
- rotate([10, -25, 30])
456
- cube([0.5, 0.26, 0.05], center=true);
457
- }
458
- `,
459
- );
460
-
461
- // Tier 6: Peach
462
- writeFile(
463
- "assets/models/fruit_6.scad",
464
- `$fn = 30;
465
- $asa = 45;
466
- r = 2.05;
467
-
468
- color([1.0, 0.42, 0.45], roughness=0.6) {
469
- translate([-0.18, 0, 0])
470
- scale([1.0, 0.96, 1.05])
471
- sphere(r=r * 0.94);
472
- translate([0.18, 0, 0])
473
- scale([1.0, 0.96, 1.05])
474
- sphere(r=r * 0.94);
475
- }
476
-
477
- color([0.2, 0.65, 0.25], roughness=0.5) {
478
- translate([0.25, 0, r + 0.1])
479
- rotate([15, -35, 40])
480
- cube([0.7, 0.35, 0.06], center=true);
481
- translate([0, 0, r + 0.05])
482
- cylinder(r=0.08, h=0.3, center=true);
483
- }
484
- `,
485
- );
486
-
487
- // Tier 7: Melon
488
- writeFile(
489
- "assets/models/fruit_7.scad",
490
- `$fn = 32;
491
- $asa = 45;
492
- r = 2.5;
493
-
494
- color([0.52, 0.88, 0.42], roughness=0.35) {
495
- sphere(r=r);
496
- }
497
-
498
- color([0.88, 0.98, 0.62], roughness=0.6) {
499
- for (i = [0:5]) {
500
- rotate([0, 0, i * 30])
501
- rotate([90, 0, 0])
502
- difference() {
503
- cylinder(r=r + 0.03, h=0.18, center=true);
504
- cylinder(r=r - 0.05, h=0.25, center=true);
505
- }
506
- }
507
- }
508
-
509
- color([0.28, 0.55, 0.2], roughness=0.7) {
510
- translate([0, 0, r + 0.1])
511
- cylinder(r=0.18, h=0.35, center=true);
512
- }
513
- `,
514
- );
515
-
516
- // Tier 8: Watermelon
517
- writeFile(
518
- "assets/models/fruit_8.scad",
519
- `$fn = 32;
520
- $asa = 45;
521
- r = 3.0;
522
-
523
- color([0.22, 0.72, 0.28], roughness=0.2) {
524
- sphere(r=r);
525
- }
526
-
527
- color([0.06, 0.28, 0.1], roughness=0.35) {
528
- for (a = [0:7]) {
529
- rotate([0, 0, a * 45])
530
- rotate([0, 18, 0])
531
- difference() {
532
- sphere(r=r + 0.02);
533
- sphere(r=r - 0.05);
534
- cube([r * 3, r * 1.5, r * 3], center=true);
535
- }
536
- }
537
- }
538
-
539
- color([0.2, 0.45, 0.15], roughness=0.6) {
540
- translate([0, 0, r + 0.15])
541
- cylinder(r1=0.25, r2=0.12, h=0.45, center=true);
542
- }
543
- `,
544
- );
545
-
546
- // Tier 9: Celestial King Watermelon
547
- writeFile(
548
- "assets/models/fruit_9.scad",
549
- `$fn = 32;
550
- $asa = 45;
551
- r = 3.6;
552
-
553
- color([1.0, 0.78, 0.15], emissive=[0.9, 0.55, 0.05], emissiveIntensity=1.5, metalness=0.4, roughness=0.2) {
554
- sphere(r=r);
555
- }
556
-
557
- color([1.0, 0.85, 0.25], metalness=0.9, roughness=0.2) {
558
- translate([0, 0, r + 0.35]) {
559
- cylinder(r=1.2, h=0.3, center=true);
560
- for (i = [0:5]) {
561
- rotate([0, 0, i * 60])
562
- translate([1.05, 0, 0.45])
563
- cylinder(r1=0.22, r2=0.03, h=0.7, center=true);
564
- }
565
- }
566
- }
567
-
568
- color([0.2, 0.9, 1.0], emissive=[0.3, 0.9, 1.0], emissiveIntensity=3.0, roughness=0.1) {
569
- for (i = [0:5]) {
570
- rotate([0, 0, i * 60 + 30])
571
- translate([1.1, 0, r + 0.45])
572
- sphere(r=0.15);
573
- }
574
- translate([0, 0, r + 1.2])
575
- sphere(r=0.28);
576
- }
577
- `,
578
- );
579
-
580
- // ==========================================
581
- // 3. GODOT 4 SCRIPTS & DATA
582
- // ==========================================
583
-
584
- writeFile(
585
- "project.godot",
586
- `config_version=5
587
-
588
- [application]
589
-
590
- config/name="Fruit Fusion 3D"
591
- config/description="3D Merge Game in Godot 4"
592
- run/main_scene="res://scenes/main.tscn"
593
- config/features=PackedStringArray("4.3", "Forward Plus")
594
-
595
- [autoload]
596
-
597
- AudioManager="*res://scripts/audio_manager.gd"
598
-
599
- [display]
600
-
601
- window/size/viewport_width=720
602
- window/size/viewport_height=1080
603
- window/size/mode=0
604
- window/size/resizable=true
605
- window/stretch/mode="canvas_items"
606
- window/stretch/aspect="expand"
607
- window/handheld/orientation=1
608
-
609
- [editor_plugins]
610
-
611
- enabled=PackedStringArray("res://addons/scad_importer/plugin.cfg")
612
-
613
- [rendering]
614
-
615
- anti_aliasing/quality/msaa_3d=2
616
- anti_aliasing/quality/screen_space_aa=1
617
- lights_and_shadows/directional_shadow/soft_shadow_filter_quality=2
618
- `,
619
- );
620
-
621
- writeFile(
622
- ".gitignore",
623
- `.godot/
624
- *.tmp
625
- *.log
626
- `,
627
- );
628
-
629
- writeFile(
630
- "scripts/audio_manager.gd",
631
- `extends Node
632
-
633
- var sample_rate: float = 22050.0
634
-
635
- func _ready():
636
- process_mode = Node.PROCESS_MODE_ALWAYS
637
-
638
- func play_pop_sound(pitch_mult: float = 1.0):
639
- _generate_and_play_sfx(350.0 * pitch_mult, 700.0 * pitch_mult, 0.12, 0.4, "sine")
640
-
641
- func play_merge_sound(tier: int):
642
- var base_freq = 240.0 + (tier * 75.0)
643
- _generate_and_play_sfx(base_freq, base_freq * 1.5, 0.22, 0.55, "triangle")
644
-
645
- func play_drop_sound():
646
- _generate_and_play_sfx(180.0, 110.0, 0.09, 0.35, "sine")
647
-
648
- func play_game_over_sound():
649
- _generate_and_play_sfx(300.0, 120.0, 0.6, 0.6, "saw")
650
-
651
- func _generate_and_play_sfx(start_freq: float, end_freq: float, duration: float, volume: float, wave_type: String):
652
- var player = AudioStreamPlayer.new()
653
- add_child(player)
654
-
655
- var total_frames = int(sample_rate * duration)
656
- var byte_data = PackedByteArray()
657
-
658
- for i in range(total_frames):
659
- var t = float(i) / float(total_frames)
660
- var current_freq = lerp(start_freq, end_freq, t)
661
- var phase = float(i) * current_freq * TAU / sample_rate
662
-
663
- var sample: float = 0.0
664
- if wave_type == "sine":
665
- sample = sin(phase)
666
- elif wave_type == "triangle":
667
- sample = asin(sin(phase)) * (2.0 / PI)
668
- elif wave_type == "saw":
669
- sample = (fposmod(phase, TAU) / PI) - 1.0
670
-
671
- var envelope = sin(t * PI * 0.5) * exp(-t * 4.5) * volume
672
- sample *= envelope
673
-
674
- var int_val = int(clamp(sample, -1.0, 1.0) * 32767.0)
675
- byte_data.append(int_val & 0xFF)
676
- byte_data.append((int_val >> 8) & 0xFF)
677
-
678
- var stream = AudioStreamWAV.new()
679
- stream.format = AudioStreamWAV.FORMAT_16_BITS
680
- stream.mix_rate = int(sample_rate)
681
- stream.stereo = false
682
- stream.data = byte_data
683
-
684
- player.stream = stream
685
- player.play()
686
- player.finished.connect(player.queue_free)
687
- `,
688
- );
689
-
690
- writeFile(
691
- "scripts/fruit_data.gd",
692
- `class_name FruitData
693
- extends RefCounted
694
-
695
- const FRUIT_NAMES = [
696
- "Cherry",
697
- "Strawberry",
698
- "Grape",
699
- "Tangerine",
700
- "Apple",
701
- "Peach",
702
- "Melon",
703
- "Watermelon",
704
- "King Sun"
705
- ]
706
-
707
- const FRUIT_RADII = [
708
- 0.55,
709
- 0.75,
710
- 0.98,
711
- 1.30,
712
- 1.65,
713
- 2.05,
714
- 2.50,
715
- 3.00,
716
- 3.60
717
- ]
718
-
719
- const FRUIT_SCORES = [
720
- 2,
721
- 4,
722
- 8,
723
- 16,
724
- 32,
725
- 64,
726
- 128,
727
- 256,
728
- 512
729
- ]
730
-
731
- const FRUIT_COLORS = [
732
- Color(0.85, 0.05, 0.15),
733
- Color(0.95, 0.12, 0.28),
734
- Color(0.55, 0.15, 0.75),
735
- Color(1.00, 0.50, 0.05),
736
- Color(0.90, 0.10, 0.12),
737
- Color(1.00, 0.55, 0.50),
738
- Color(0.55, 0.88, 0.45),
739
- Color(0.18, 0.65, 0.25),
740
- Color(1.00, 0.82, 0.15)
741
- ]
742
-
743
- static func get_model_path(tier: int) -> String:
744
- return "res://assets/models/fruit_%d.scad" % clampi(tier, 1, 9)
745
- `,
746
- );
747
-
748
- writeFile(
749
- "scripts/fruit.gd",
750
- `class_name Fruit
751
- extends RigidBody3D
752
-
753
- signal fruit_merged(pos: Vector3, next_tier: int, score: int)
754
- signal settled_in_danger_zone(fruit_node: Fruit)
755
-
756
- @export var tier: int = 1
757
-
758
- var radius: float = 0.55
759
- var is_merging: bool = false
760
- var has_dropped: bool = false
761
- var drop_time: float = 0.0
762
- var model_instance: Node3D = null
763
-
764
- func setup(p_tier: int):
765
- tier = clampi(p_tier, 1, 9)
766
- radius = FruitData.FRUIT_RADII[tier - 1]
767
- mass = pow(radius, 2.5) * 1.5
768
-
769
- for child in get_children():
770
- if child is CollisionShape3D or child is Node3D:
771
- child.queue_free()
772
-
773
- var col = CollisionShape3D.new()
774
- var sphere = SphereShape3D.new()
775
- sphere.radius = radius
776
- col.shape = sphere
777
- add_child(col)
778
-
779
- var model_path = FruitData.get_model_path(tier)
780
- if ResourceLoader.exists(model_path):
781
- var model_scene = load(model_path)
782
- if model_scene:
783
- model_instance = model_scene.instantiate()
784
- add_child(model_instance)
785
-
786
- if not model_instance:
787
- var fallback_mesh = MeshInstance3D.new()
788
- var s_mesh = SphereMesh.new()
789
- s_mesh.radius = radius
790
- s_mesh.height = radius * 2.0
791
- fallback_mesh.mesh = s_mesh
792
- var mat = StandardMaterial3D.new()
793
- mat.albedo_color = FruitData.FRUIT_COLORS[tier - 1]
794
- mat.roughness = 0.3
795
- fallback_mesh.material_override = mat
796
- add_child(fallback_mesh)
797
-
798
- func _ready():
799
- axis_lock_linear_z = true
800
- axis_lock_angular_x = true
801
- axis_lock_angular_y = true
802
-
803
- contact_monitor = true
804
- max_contacts_reported = 4
805
- body_entered.connect(_on_body_entered)
806
-
807
- scale = Vector3.ONE * 0.2
808
- var tween = create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
809
- tween.tween_property(self, "scale", Vector3.ONE, 0.25)
810
-
811
- func _process(delta):
812
- if has_dropped:
813
- drop_time += delta
814
- if drop_time > 2.5 and global_position.y >= 13.5 and linear_velocity.length() < 0.6:
815
- settled_in_danger_zone.emit(self)
816
-
817
- func _on_body_entered(body: Node):
818
- if is_merging:
819
- return
820
-
821
- if body is Fruit:
822
- var other: Fruit = body
823
- if other.is_merging:
824
- return
825
-
826
- if other.tier == self.tier and self.tier < 9:
827
- if self.get_instance_id() < other.get_instance_id():
828
- self.is_merging = true
829
- other.is_merging = true
830
- var merge_pos = (global_position + other.global_position) * 0.5
831
- fruit_merged.emit(merge_pos, self.tier + 1, FruitData.FRUIT_SCORES[self.tier - 1])
832
- other.queue_free()
833
- self.queue_free()
834
- `,
835
- );
836
-
837
- writeFile(
838
- "scripts/game_manager.gd",
839
- `class_name GameManager
840
- extends Node3D
841
-
842
- signal score_updated(score: int, high_score: int)
843
- signal next_fruit_changed(tier: int)
844
- signal game_over_triggered
845
-
846
- @onready var dropper: Node3D = $Dropper
847
- @onready var fruits_container: Node3D = $FruitsContainer
848
- @onready var aim_line: MeshInstance3D = $AimLine
849
- @onready var container_visual: Node3D = $ContainerVisual
850
-
851
- # Dimensions of the 3D glass box container (box_w = 12.0, walls at +/- 6.0)
852
- const CONTAINER_HALF_WIDTH: float = 6.0
853
- const BOX_HALF_WIDTH: float = 4.8
854
- const DROP_HEIGHT: float = 14.8
855
-
856
- var current_score: int = 0
857
- var high_score: int = 0
858
- var next_tier: int = 1
859
- var active_preview_tier: int = 1
860
- var preview_fruit_node: Node3D = null
861
-
862
- var can_drop: bool = true
863
- var drop_cooldown: float = 0.55
864
- var is_game_over: bool = false
865
- var danger_timer: float = 0.0
866
- const DANGER_THRESHOLD: float = 2.8
867
-
868
- # Touch / Mouse Drag Controls
869
- var is_pointer_dragging: bool = false
870
- var active_touch_index: int = -1
871
-
872
- var high_score_file = "user://fusion_highscore.save"
873
-
874
- func _ready():
875
- load_high_score()
876
- score_updated.emit(current_score, high_score)
877
-
878
- _setup_container_visuals()
879
- _setup_dropper_visuals()
880
-
881
- randomize()
882
- active_preview_tier = randi_range(1, 3)
883
- next_tier = randi_range(1, 3)
884
- next_fruit_changed.emit(next_tier)
885
-
886
- _spawn_preview_fruit()
887
- _update_aim_line()
888
-
889
- func _notification(what):
890
- if what == NOTIFICATION_APPLICATION_FOCUS_OUT:
891
- is_pointer_dragging = false
892
- active_touch_index = -1
893
-
894
- func _get_dropper_max_x() -> float:
895
- var fruit_r: float = 0.55
896
- if active_preview_tier >= 1 and active_preview_tier <= FruitData.FRUIT_RADII.size():
897
- fruit_r = FruitData.FRUIT_RADII[active_preview_tier - 1]
898
- # Keep the preview fruit completely within the glass container walls with safety clearance
899
- return max(0.5, CONTAINER_HALF_WIDTH - fruit_r - 0.08)
900
-
901
- func _setup_container_visuals():
902
- if not is_instance_valid(container_visual):
903
- return
904
-
905
- var loaded_scad = false
906
- var container_model_path = "res://assets/models/container.scad"
907
- if ResourceLoader.exists(container_model_path):
908
- var container_scene = load(container_model_path)
909
- if container_scene:
910
- var c_inst = container_scene.instantiate()
911
- container_visual.add_child(c_inst)
912
- _enforce_glass_transparency(c_inst)
913
- loaded_scad = true
914
-
915
- if not loaded_scad:
916
- _build_procedural_glass_box()
917
-
918
- func _enforce_glass_transparency(root_node: Node):
919
- for child in root_node.get_children():
920
- if child is MeshInstance3D:
921
- var mesh = child.mesh
922
- if mesh:
923
- for s in range(mesh.get_surface_count()):
924
- var mat = child.get_active_material(s)
925
- if mat is BaseMaterial3D:
926
- # If it is a glass material (light tint or alpha transparency)
927
- if mat.albedo_color.a < 0.95 or mat.albedo_color.b > 0.85:
928
- var glass = mat.duplicate()
929
- glass.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
930
- glass.cull_mode = BaseMaterial3D.CULL_BACK
931
- glass.albedo_color = Color(0.85, 0.95, 1.0, 0.12)
932
- glass.roughness = 0.05
933
- glass.metallic = 0.1
934
- child.set_surface_override_material(s, glass)
935
- _enforce_glass_transparency(child)
936
-
937
- func _build_procedural_glass_box():
938
- # Procedural transparent glass box with visible frames
939
- var glass_mat = StandardMaterial3D.new()
940
- glass_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
941
- glass_mat.cull_mode = BaseMaterial3D.CULL_BACK
942
- glass_mat.albedo_color = Color(0.85, 0.95, 1.0, 0.12)
943
- glass_mat.roughness = 0.05
944
- glass_mat.metallic = 0.05
945
-
946
- # Back Glass
947
- var back_mesh = MeshInstance3D.new()
948
- var bm = BoxMesh.new()
949
- bm.size = Vector3(12.0, 16.0, 0.1)
950
- back_mesh.mesh = bm
951
- back_mesh.material_override = glass_mat
952
- back_mesh.position = Vector3(0, 8.0, -2.0)
953
- container_visual.add_child(back_mesh)
954
-
955
- # Left Glass
956
- var left_mesh = MeshInstance3D.new()
957
- var lm = BoxMesh.new()
958
- lm.size = Vector3(0.1, 16.0, 4.0)
959
- left_mesh.mesh = lm
960
- left_mesh.material_override = glass_mat
961
- left_mesh.position = Vector3(-6.0, 8.0, 0)
962
- container_visual.add_child(left_mesh)
963
-
964
- # Right Glass
965
- var right_mesh = MeshInstance3D.new()
966
- var rm = BoxMesh.new()
967
- rm.size = Vector3(0.1, 16.0, 4.0)
968
- right_mesh.mesh = rm
969
- right_mesh.material_override = glass_mat
970
- right_mesh.position = Vector3(6.0, 8.0, 0)
971
- container_visual.add_child(right_mesh)
972
-
973
- # Front Ultra-clear Glass
974
- var front_mat = glass_mat.duplicate()
975
- front_mat.albedo_color = Color(0.9, 0.97, 1.0, 0.07)
976
- var front_mesh = MeshInstance3D.new()
977
- var fm = BoxMesh.new()
978
- fm.size = Vector3(12.0, 16.0, 0.05)
979
- front_mesh.mesh = fm
980
- front_mesh.material_override = front_mat
981
- front_mesh.position = Vector3(0, 8.0, 2.0)
982
- container_visual.add_child(front_mesh)
983
-
984
- # Wooden Base
985
- var base_mesh = MeshInstance3D.new()
986
- var basem = BoxMesh.new()
987
- basem.size = Vector3(13.6, 0.8, 5.6)
988
- base_mesh.mesh = basem
989
- var wood_mat = StandardMaterial3D.new()
990
- wood_mat.albedo_color = Color(0.55, 0.32, 0.16)
991
- wood_mat.roughness = 0.7
992
- base_mesh.material_override = wood_mat
993
- base_mesh.position = Vector3(0, -0.4, 0)
994
- container_visual.add_child(base_mesh)
995
-
996
- # Danger Line Marker at Y = 13.5
997
- var danger_mat = StandardMaterial3D.new()
998
- danger_mat.albedo_color = Color(1.0, 0.2, 0.2, 1.0)
999
- danger_mat.emission_enabled = true
1000
- danger_mat.emission = Color(1.0, 0.2, 0.2)
1001
- danger_mat.emission_energy_multiplier = 2.5
1002
- var danger_line = MeshInstance3D.new()
1003
- var dlm = BoxMesh.new()
1004
- dlm.size = Vector3(12.4, 0.12, 4.4)
1005
- danger_line.mesh = dlm
1006
- danger_line.material_override = danger_mat
1007
- danger_line.position = Vector3(0, 13.5, 0)
1008
- container_visual.add_child(danger_line)
1009
-
1010
- func _setup_dropper_visuals():
1011
- var dropper_model_path = "res://assets/models/dropper.scad"
1012
- if ResourceLoader.exists(dropper_model_path) and is_instance_valid(dropper):
1013
- var dropper_scene = load(dropper_model_path)
1014
- if dropper_scene:
1015
- if dropper.has_node("CloudMesh"):
1016
- dropper.get_node("CloudMesh").visible = false
1017
- if dropper.has_node("PointerMesh"):
1018
- dropper.get_node("PointerMesh").visible = false
1019
- var d_inst = dropper_scene.instantiate()
1020
- dropper.add_child(d_inst)
1021
-
1022
- func load_high_score():
1023
- if FileAccess.file_exists(high_score_file):
1024
- var file = FileAccess.open(high_score_file, FileAccess.READ)
1025
- if file:
1026
- high_score = file.get_32()
1027
- file.close()
1028
-
1029
- func save_high_score():
1030
- var file = FileAccess.open(high_score_file, FileAccess.WRITE)
1031
- if file:
1032
- file.store_32(high_score)
1033
- file.close()
1034
-
1035
- func _update_dropper_x(pos):
1036
- if not is_instance_valid(dropper):
1037
- return
1038
-
1039
- var screen_pos: Vector2
1040
- if pos is Vector2:
1041
- screen_pos = pos
1042
- else:
1043
- screen_pos = Vector2(float(pos), get_viewport().get_visible_rect().size.y * 0.5)
1044
-
1045
- var max_x = _get_dropper_max_x()
1046
-
1047
- # Raycast onto the glass container's central plane (Z = 0) where the fruits reside.
1048
- # This ensures picking maps directly along the physical glass container's width.
1049
- var camera = get_viewport().get_camera_3d()
1050
- if camera:
1051
- var ray_from = camera.project_ray_origin(screen_pos)
1052
- var ray_dir = camera.project_ray_normal(screen_pos)
1053
- var drop_plane = Plane(Vector3(0, 0, 1), 0.0)
1054
- var hit = drop_plane.intersects_ray(ray_from, ray_dir)
1055
- if hit != null:
1056
- dropper.position.x = clamp(hit.x, -max_x, max_x)
1057
- _update_aim_line()
1058
- return
1059
-
1060
- var vp_size = get_viewport().get_visible_rect().size
1061
- if vp_size.x > 0.0:
1062
- var norm_x = (screen_pos.x / vp_size.x) * 2.0 - 1.0
1063
- dropper.position.x = clamp(norm_x * max_x, -max_x, max_x)
1064
- _update_aim_line()
1065
-
1066
- func _unhandled_input(event: InputEvent):
1067
- if is_game_over or not is_instance_valid(dropper):
1068
- return
1069
-
1070
- # Touch input (mobile / touchscreens)
1071
- if event is InputEventScreenTouch:
1072
- if event.pressed:
1073
- if not is_pointer_dragging:
1074
- is_pointer_dragging = true
1075
- active_touch_index = event.index
1076
- _update_dropper_x(event.position)
1077
- else:
1078
- if is_pointer_dragging and event.index == active_touch_index:
1079
- is_pointer_dragging = false
1080
- active_touch_index = -1
1081
- _update_dropper_x(event.position)
1082
- try_drop_fruit()
1083
- return
1084
-
1085
- if event is InputEventScreenDrag:
1086
- if is_pointer_dragging and event.index == active_touch_index:
1087
- _update_dropper_x(event.position)
1088
- return
1089
-
1090
- # Mouse input (desktop & web)
1091
- if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
1092
- if event.pressed:
1093
- if not is_pointer_dragging:
1094
- is_pointer_dragging = true
1095
- _update_dropper_x(event.position)
1096
- else:
1097
- if is_pointer_dragging and active_touch_index == -1:
1098
- is_pointer_dragging = false
1099
- _update_dropper_x(event.position)
1100
- try_drop_fruit()
1101
- return
1102
-
1103
- if event is InputEventMouseMotion:
1104
- if active_touch_index == -1:
1105
- _update_dropper_x(event.position)
1106
- return
1107
-
1108
- # Keyboard drop
1109
- if event.is_action_pressed("ui_accept") or event.is_action_pressed("ui_down"):
1110
- try_drop_fruit()
1111
-
1112
- func _physics_process(delta: float):
1113
- if not is_inside_tree() or not is_instance_valid(fruits_container) or not is_instance_valid(dropper):
1114
- return
1115
-
1116
- if is_game_over:
1117
- return
1118
-
1119
- var max_x = _get_dropper_max_x()
1120
- var move_dir = Input.get_axis("ui_left", "ui_right")
1121
- if move_dir != 0.0:
1122
- dropper.position.x = clamp(dropper.position.x + move_dir * 14.0 * delta, -max_x, max_x)
1123
- _update_aim_line()
1124
-
1125
- var fruits_in_danger = 0
1126
- for fruit in fruits_container.get_children():
1127
- if is_instance_valid(fruit) and fruit is Fruit and fruit.has_dropped:
1128
- if fruit.global_position.y >= 13.5 and fruit.drop_time > 2.0 and fruit.linear_velocity.length() < 0.5:
1129
- fruits_in_danger += 1
1130
-
1131
- if fruits_in_danger > 0:
1132
- danger_timer += delta
1133
- if danger_timer >= DANGER_THRESHOLD:
1134
- trigger_game_over()
1135
- else:
1136
- danger_timer = max(0.0, danger_timer - delta * 1.5)
1137
-
1138
- func _update_aim_line():
1139
- if is_instance_valid(aim_line) and is_instance_valid(dropper):
1140
- aim_line.position.x = dropper.position.x
1141
- aim_line.position.y = DROP_HEIGHT * 0.5
1142
- aim_line.scale.y = DROP_HEIGHT
1143
-
1144
- func _spawn_preview_fruit():
1145
- if not is_instance_valid(dropper):
1146
- return
1147
-
1148
- if is_instance_valid(preview_fruit_node):
1149
- preview_fruit_node.queue_free()
1150
- preview_fruit_node = null
1151
-
1152
- var max_x = _get_dropper_max_x()
1153
- dropper.position.x = clamp(dropper.position.x, -max_x, max_x)
1154
- _update_aim_line()
1155
-
1156
- var model_path = FruitData.get_model_path(active_preview_tier)
1157
- if ResourceLoader.exists(model_path):
1158
- var scene = load(model_path)
1159
- if scene:
1160
- preview_fruit_node = scene.instantiate()
1161
- dropper.add_child(preview_fruit_node)
1162
- preview_fruit_node.position = Vector3(0, -0.9, 0)
1163
-
1164
- if not preview_fruit_node:
1165
- var mesh_inst = MeshInstance3D.new()
1166
- var sm = SphereMesh.new()
1167
- sm.radius = FruitData.FRUIT_RADII[active_preview_tier - 1]
1168
- sm.height = sm.radius * 2.0
1169
- mesh_inst.mesh = sm
1170
- var mat = StandardMaterial3D.new()
1171
- mat.albedo_color = FruitData.FRUIT_COLORS[active_preview_tier - 1]
1172
- mesh_inst.material_override = mat
1173
- preview_fruit_node = mesh_inst
1174
- dropper.add_child(preview_fruit_node)
1175
- preview_fruit_node.position = Vector3(0, -0.9, 0)
1176
-
1177
- func try_drop_fruit():
1178
- if not can_drop or is_game_over or not is_instance_valid(fruits_container) or not is_instance_valid(dropper):
1179
- return
1180
-
1181
- can_drop = false
1182
- AudioManager.play_drop_sound()
1183
-
1184
- var fruit = Fruit.new()
1185
- fruits_container.add_child(fruit)
1186
- fruit.setup(active_preview_tier)
1187
- fruit.position = dropper.position + Vector3(0, -0.9, 0)
1188
- fruit.has_dropped = true
1189
- fruit.fruit_merged.connect(_on_fruit_merged)
1190
-
1191
- if is_instance_valid(preview_fruit_node):
1192
- preview_fruit_node.queue_free()
1193
- preview_fruit_node = null
1194
-
1195
- active_preview_tier = next_tier
1196
- next_tier = randi_range(1, 3)
1197
- next_fruit_changed.emit(next_tier)
1198
-
1199
- get_tree().create_timer(drop_cooldown).timeout.connect(func():
1200
- if not is_game_over:
1201
- can_drop = true
1202
- _spawn_preview_fruit()
1203
- )
1204
-
1205
- func _on_fruit_merged(merge_pos: Vector3, new_tier: int, score_awarded: int):
1206
- current_score += score_awarded
1207
- if current_score > high_score:
1208
- high_score = current_score
1209
- save_high_score()
1210
- score_updated.emit(current_score, high_score)
1211
-
1212
- AudioManager.play_merge_sound(new_tier)
1213
- _spawn_merge_vfx(merge_pos, FruitData.FRUIT_COLORS[new_tier - 2])
1214
-
1215
- if not is_instance_valid(fruits_container):
1216
- return
1217
-
1218
- var evolved_fruit = Fruit.new()
1219
- fruits_container.add_child(evolved_fruit)
1220
- evolved_fruit.setup(new_tier)
1221
- evolved_fruit.position = merge_pos
1222
- evolved_fruit.has_dropped = true
1223
- evolved_fruit.drop_time = 1.0
1224
- evolved_fruit.fruit_merged.connect(_on_fruit_merged)
1225
-
1226
- func _spawn_merge_vfx(pos: Vector3, color: Color):
1227
- var particles = CPUParticles3D.new()
1228
- add_child(particles)
1229
- particles.position = pos
1230
- particles.emitting = true
1231
- particles.one_shot = true
1232
- particles.explosiveness = 0.95
1233
- particles.lifetime = 0.45
1234
- particles.amount = 24
1235
- particles.spread = 180.0
1236
- particles.initial_velocity_min = 4.0
1237
- particles.initial_velocity_max = 8.0
1238
- particles.scale_amount_min = 0.15
1239
- particles.scale_amount_max = 0.35
1240
- particles.color = color
1241
-
1242
- var mesh = BoxMesh.new()
1243
- mesh.size = Vector3(0.2, 0.2, 0.2)
1244
- particles.mesh = mesh
1245
-
1246
- get_tree().create_timer(0.55).timeout.connect(particles.queue_free)
1247
-
1248
- func trigger_game_over():
1249
- if is_game_over:
1250
- return
1251
- is_game_over = true
1252
- is_pointer_dragging = false
1253
- active_touch_index = -1
1254
- AudioManager.play_game_over_sound()
1255
- game_over_triggered.emit()
1256
-
1257
- func restart_game():
1258
- if is_instance_valid(fruits_container):
1259
- for child in fruits_container.get_children():
1260
- child.queue_free()
1261
- current_score = 0
1262
- danger_timer = 0.0
1263
- is_game_over = false
1264
- can_drop = true
1265
- is_pointer_dragging = false
1266
- active_touch_index = -1
1267
- score_updated.emit(current_score, high_score)
1268
- active_preview_tier = randi_range(1, 3)
1269
- next_tier = randi_range(1, 3)
1270
- next_fruit_changed.emit(next_tier)
1271
- _spawn_preview_fruit()
1272
- `,
1273
- );
1274
-
1275
- writeFile(
1276
- "scripts/ui_manager.gd",
1277
- `extends Control
1278
-
1279
- @onready var game_manager: GameManager = $"../"
1280
- @onready var score_label = $ScoreContainer/ScoreValue
1281
- @onready var high_score_label = $ScoreContainer/HighScoreValue
1282
- @onready var next_fruit_preview = $NextContainer/PreviewRect
1283
- @onready var next_fruit_label = $NextContainer/NextFruitName
1284
- @onready var game_over_panel = $GameOverPanel
1285
- @onready var final_score_label = $GameOverPanel/FinalScore
1286
- @onready var restart_button = $GameOverPanel/RestartButton
1287
- @onready var danger_indicator = $DangerIndicator
1288
-
1289
- var danger_blink_time: float = 0.0
1290
-
1291
- func _ready():
1292
- game_over_panel.visible = false
1293
- danger_indicator.visible = false
1294
- restart_button.pressed.connect(_on_restart_pressed)
1295
- game_manager.score_updated.connect(_on_score_updated)
1296
- game_manager.next_fruit_changed.connect(_on_next_fruit_changed)
1297
- game_manager.game_over_triggered.connect(_on_game_over)
1298
-
1299
- func _process(delta):
1300
- if is_instance_valid(game_manager) and game_manager.danger_timer > 0.5 and not game_manager.is_game_over:
1301
- danger_indicator.visible = true
1302
- danger_blink_time += delta * 6.0
1303
- danger_indicator.modulate.a = (sin(danger_blink_time) * 0.5 + 0.5) * 0.85
1304
- else:
1305
- danger_indicator.visible = false
1306
- danger_blink_time = 0.0
1307
-
1308
- func _on_score_updated(score: int, high_score: int):
1309
- score_label.text = str(score)
1310
- high_score_label.text = "BEST: " + str(high_score)
1311
-
1312
- func _on_next_fruit_changed(tier: int):
1313
- var fruit_name = FruitData.FRUIT_NAMES[tier - 1]
1314
- next_fruit_label.text = fruit_name
1315
- next_fruit_preview.color = FruitData.FRUIT_COLORS[tier - 1]
1316
-
1317
- func _on_game_over():
1318
- game_over_panel.visible = true
1319
- final_score_label.text = "FINAL SCORE: %d" % game_manager.current_score
1320
-
1321
- func _on_restart_pressed():
1322
- game_over_panel.visible = false
1323
- game_manager.restart_game()
1324
- `,
1325
- );
1326
-
1327
- // Procedural 2D Evolution Guide (No missing emojis on Web/Mobile)
1328
- writeFile(
1329
- "scripts/evolution_guide.gd",
1330
- `class_name EvolutionGuide
1331
- extends Control
1332
-
1333
- func _ready():
1334
- mouse_filter = Control.MOUSE_FILTER_IGNORE
1335
- resized.connect(queue_redraw)
1336
-
1337
- func _draw():
1338
- var w = size.x
1339
- var h = size.y
1340
- if w <= 0 or h <= 0:
1341
- return
1342
-
1343
- # Sleek pill-shaped background
1344
- var bg_style = StyleBoxFlat.new()
1345
- bg_style.bg_color = Color(0.06, 0.09, 0.15, 0.75)
1346
- var cr = int(min(16.0, h * 0.45))
1347
- bg_style.corner_radius_top_left = cr
1348
- bg_style.corner_radius_top_right = cr
1349
- bg_style.corner_radius_bottom_left = cr
1350
- bg_style.corner_radius_bottom_right = cr
1351
- bg_style.border_width_left = 1
1352
- bg_style.border_width_top = 1
1353
- bg_style.border_width_right = 1
1354
- bg_style.border_width_bottom = 1
1355
- bg_style.border_color = Color(1.0, 1.0, 1.0, 0.14)
1356
- draw_style_box(bg_style, Rect2(Vector2.ZERO, size))
1357
-
1358
- var fruit_count = 9
1359
- var slot_width = w / float(fruit_count)
1360
- var y_center = h * 0.5
1361
-
1362
- var radii = [7.5, 9.0, 10.5, 12.0, 13.5, 15.0, 16.5, 18.0, 19.5]
1363
- var max_r_allowed = h * 0.38
1364
- var scale_factor = 1.0
1365
- if radii[8] > max_r_allowed:
1366
- scale_factor = max_r_allowed / radii[8]
1367
-
1368
- for i in range(fruit_count):
1369
- var r = radii[i] * scale_factor
1370
- var cx = slot_width * (i + 0.5)
1371
- var pos = Vector2(cx, y_center)
1372
- var col = FruitData.FRUIT_COLORS[i]
1373
-
1374
- # Shadow
1375
- draw_circle(pos + Vector2(0, 1.5), r + 0.8, Color(0, 0, 0, 0.35))
1376
- # Main body
1377
- draw_circle(pos, r, col)
1378
- # Specular reflection
1379
- draw_circle(pos + Vector2(-r * 0.35, -r * 0.35), r * 0.32, Color(1.0, 1.0, 1.0, 0.38))
1380
-
1381
- # Distinct fruit icon visual elements
1382
- match i + 1:
1383
- 1: # Cherry: stem and leaf
1384
- draw_line(pos + Vector2(0, -r), pos + Vector2(r * 0.4, -r - 4.5 * scale_factor), Color(0.3, 0.7, 0.15), 1.4, true)
1385
- draw_circle(pos + Vector2(r * 0.45, -r - 4.5 * scale_factor), 1.8 * scale_factor, Color(0.3, 0.85, 0.2))
1386
- 2: # Strawberry: green sepal and seeds
1387
- draw_circle(pos + Vector2(-r * 0.35, -r + 1), 1.8 * scale_factor, Color(0.2, 0.8, 0.25))
1388
- draw_circle(pos + Vector2(0, -r - 1), 2.0 * scale_factor, Color(0.2, 0.8, 0.25))
1389
- draw_circle(pos + Vector2(r * 0.35, -r + 1), 1.8 * scale_factor, Color(0.2, 0.8, 0.25))
1390
- draw_circle(pos + Vector2(-r * 0.2, -r * 0.1), 0.9 * scale_factor, Color(1.0, 0.9, 0.35))
1391
- draw_circle(pos + Vector2(r * 0.25, -r * 0.15), 0.9 * scale_factor, Color(1.0, 0.9, 0.35))
1392
- draw_circle(pos + Vector2(0, r * 0.35), 0.9 * scale_factor, Color(1.0, 0.9, 0.35))
1393
- 3: # Grape: stem and cluster spots
1394
- draw_line(pos + Vector2(0, -r), pos + Vector2(0, -r - 3.5 * scale_factor), Color(0.4, 0.7, 0.2), 1.4, true)
1395
- draw_circle(pos + Vector2(-r * 0.25, -r * 0.15), r * 0.25, Color(0.4, 0.1, 0.55, 0.7))
1396
- draw_circle(pos + Vector2(r * 0.25, -r * 0.1), r * 0.25, Color(0.4, 0.1, 0.55, 0.7))
1397
- draw_circle(pos + Vector2(0, r * 0.3), r * 0.25, Color(0.4, 0.1, 0.55, 0.7))
1398
- 4: # Tangerine: leaf
1399
- draw_line(pos + Vector2(0, -r), pos + Vector2(r * 0.25, -r - 3.0 * scale_factor), Color(0.3, 0.65, 0.2), 1.4, true)
1400
- draw_circle(pos + Vector2(r * 0.35, -r - 2.5 * scale_factor), 2.2 * scale_factor, Color(0.25, 0.8, 0.2))
1401
- 5: # Apple: stem and leaf
1402
- draw_circle(pos + Vector2(0, -r + 1), 2.0 * scale_factor, Color(0.6, 0.05, 0.08))
1403
- draw_line(pos + Vector2(0, -r + 1), pos + Vector2(r * 0.2, -r - 4.0 * scale_factor), Color(0.45, 0.25, 0.1), 1.5, true)
1404
- draw_circle(pos + Vector2(r * 0.35, -r - 3.0 * scale_factor), 2.3 * scale_factor, Color(0.25, 0.8, 0.2))
1405
- 6: # Peach: peach cleft
1406
- draw_line(pos + Vector2(0, -r * 0.7), pos + Vector2(0, r * 0.7), Color(0.85, 0.32, 0.36, 0.85), 1.6, true)
1407
- draw_circle(pos + Vector2(r * 0.25, -r - 1.5 * scale_factor), 2.2 * scale_factor, Color(0.25, 0.8, 0.2))
1408
- 7: # Melon: netting ribs
1409
- draw_arc(pos, r * 0.65, -PI * 0.5, PI * 0.5, 10, Color(0.88, 0.98, 0.62, 0.75), 1.4, true)
1410
- draw_arc(pos, r * 0.65, PI * 0.5, PI * 1.5, 10, Color(0.88, 0.98, 0.62, 0.75), 1.4, true)
1411
- draw_line(pos + Vector2(0, -r), pos + Vector2(0, -r - 3.5 * scale_factor), Color(0.3, 0.6, 0.2), 1.8, true)
1412
- 8: # Watermelon: dark green wavy stripes
1413
- var stripe_col = Color(0.06, 0.28, 0.10, 0.9)
1414
- draw_arc(pos + Vector2(-r * 0.35, 0), r * 0.8, -PI * 0.4, PI * 0.4, 10, stripe_col, 1.8, true)
1415
- draw_line(pos + Vector2(0, -r * 0.88), pos + Vector2(0, r * 0.88), stripe_col, 1.8, true)
1416
- draw_arc(pos + Vector2(r * 0.35, 0), r * 0.8, PI * 0.6, PI * 1.4, 10, stripe_col, 1.8, true)
1417
- 9: # King Sun: glowing halo and crown
1418
- draw_arc(pos, r + 2.5 * scale_factor, 0, TAU, 28, Color(1.0, 0.85, 0.2, 0.6), 1.4, true)
1419
- var cb = pos + Vector2(0, -r + 2 * scale_factor)
1420
- var crown_pts = PackedVector2Array([
1421
- cb + Vector2(-6 * scale_factor, 0),
1422
- cb + Vector2(-6 * scale_factor, -5 * scale_factor),
1423
- cb + Vector2(-3 * scale_factor, -2 * scale_factor),
1424
- cb + Vector2(0, -7 * scale_factor),
1425
- cb + Vector2(3 * scale_factor, -2 * scale_factor),
1426
- cb + Vector2(6 * scale_factor, -5 * scale_factor),
1427
- cb + Vector2(6 * scale_factor, 0)
1428
- ])
1429
- draw_colored_polygon(crown_pts, Color(1.0, 0.92, 0.3))
1430
- draw_polyline(crown_pts, Color(0.85, 0.65, 0.1), 1.0, true)
1431
- draw_circle(cb + Vector2(0, -7 * scale_factor), 1.4 * scale_factor, Color(0.2, 0.9, 1.0))
1432
-
1433
- # Evolution progression arrow
1434
- if i < fruit_count - 1:
1435
- var arrow_x = slot_width * (i + 1)
1436
- var arrow_pts = PackedVector2Array([
1437
- Vector2(arrow_x - 3.0, y_center - 4.0),
1438
- Vector2(arrow_x + 2.0, y_center),
1439
- Vector2(arrow_x - 3.0, y_center + 4.0)
1440
- ])
1441
- draw_polyline(arrow_pts, Color(1.0, 1.0, 1.0, 0.45), 1.5, true)
1442
- `,
1443
- );
1444
-
1445
- // ==========================================
1446
- // 4. MAIN SCENE FILE (.tscn)
1447
- // ==========================================
1448
-
1449
- writeFile(
1450
- "scenes/main.tscn",
1451
- `[gd_scene load_steps=16 format=3 uid="uid://c2suika0main"]
1452
-
1453
- [ext_resource type="Script" path="res://scripts/game_manager.gd" id="1_gm"]
1454
- [ext_resource type="Script" path="res://scripts/ui_manager.gd" id="2_ui"]
1455
- [ext_resource type="Script" path="res://scripts/evolution_guide.gd" id="3_evo"]
1456
-
1457
- [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
1458
- sky_top_color = Color(0.2, 0.45, 0.75, 1)
1459
- sky_horizon_color = Color(0.65, 0.78, 0.88, 1)
1460
- ground_bottom_color = Color(0.12, 0.16, 0.22, 1)
1461
- ground_horizon_color = Color(0.65, 0.78, 0.88, 1)
1462
-
1463
- [sub_resource type="Sky" id="Sky_env"]
1464
- sky_material = SubResource("ProceduralSkyMaterial_sky")
1465
-
1466
- [sub_resource type="Environment" id="Environment_main"]
1467
- background_mode = 2
1468
- sky = SubResource("Sky_env")
1469
- ambient_light_source = 2
1470
- ambient_light_color = Color(0.4, 0.4, 0.45, 1)
1471
- tonemap_mode = 2
1472
- glow_enabled = true
1473
- glow_intensity = 0.4
1474
- glow_bloom = 0.15
1475
-
1476
- [sub_resource type="BoxShape3D" id="BoxShape3D_bottom"]
1477
- size = Vector3(14, 0.5, 6)
1478
-
1479
- [sub_resource type="BoxShape3D" id="BoxShape3D_wall"]
1480
- size = Vector3(0.5, 18, 6)
1481
-
1482
- [sub_resource type="BoxShape3D" id="BoxShape3D_back"]
1483
- size = Vector3(14, 18, 0.5)
1484
-
1485
- [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_aim"]
1486
- transparency = 1
1487
- albedo_color = Color(1, 0.9, 0.3, 0.35)
1488
- emission_enabled = true
1489
- emission = Color(1, 0.85, 0.2, 1)
1490
- emission_energy_multiplier = 0.8
1491
-
1492
- [sub_resource type="CylinderMesh" id="CylinderMesh_aim"]
1493
- material = SubResource("StandardMaterial3D_aim")
1494
- top_radius = 0.04
1495
- bottom_radius = 0.04
1496
- height = 1.0
1497
-
1498
- [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_cloud"]
1499
- albedo_color = Color(0.95, 0.96, 1, 1)
1500
- roughness = 0.4
1501
-
1502
- [sub_resource type="SphereMesh" id="SphereMesh_cloud"]
1503
- material = SubResource("StandardMaterial3D_cloud")
1504
- radius = 0.8
1505
- height = 1.6
1506
-
1507
- [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pointer"]
1508
- albedo_color = Color(1, 0.8, 0.1, 1)
1509
- emission_enabled = true
1510
- emission = Color(1, 0.7, 0, 1)
1511
- emission_energy_multiplier = 2.0
1512
-
1513
- [sub_resource type="CylinderMesh" id="CylinderMesh_pointer"]
1514
- material = SubResource("StandardMaterial3D_pointer")
1515
- top_radius = 0.02
1516
- bottom_radius = 0.22
1517
- height = 0.45
1518
-
1519
- [node name="Main" type="Node3D"]
1520
- script = ExtResource("1_gm")
1521
-
1522
- [node name="WorldEnvironment" type="WorldEnvironment" parent="."]
1523
- environment = SubResource("Environment_main")
1524
-
1525
- [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
1526
- transform = Transform3D(0.866, -0.353, 0.353, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 5, 20, 15)
1527
- light_color = Color(1, 0.98, 0.94, 1)
1528
- light_energy = 1.2
1529
- shadow_enabled = true
1530
-
1531
- [node name="FillLight" type="DirectionalLight3D" parent="."]
1532
- transform = Transform3D(-0.866, 0.25, -0.433, 0, 0.866, 0.5, 0.5, 0.433, -0.75, -5, 10, -10)
1533
- light_color = Color(0.5, 0.7, 0.9, 1)
1534
- light_energy = 0.4
1535
-
1536
- [node name="Camera3D" type="Camera3D" parent="."]
1537
- transform = Transform3D(1, 0, 0, 0, 0.996, 0.087, 0, -0.087, 0.996, 0, 8.5, 21.5)
1538
- fov = 48.0
1539
-
1540
- [node name="ContainerPhysics" type="StaticBody3D" parent="."]
1541
-
1542
- [node name="CollisionBottom" type="CollisionShape3D" parent="ContainerPhysics"]
1543
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
1544
- shape = SubResource("BoxShape3D_bottom")
1545
-
1546
- [node name="CollisionLeft" type="CollisionShape3D" parent="ContainerPhysics"]
1547
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6.2, 8, 0)
1548
- shape = SubResource("BoxShape3D_wall")
1549
-
1550
- [node name="CollisionRight" type="CollisionShape3D" parent="ContainerPhysics"]
1551
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6.2, 8, 0)
1552
- shape = SubResource("BoxShape3D_wall")
1553
-
1554
- [node name="CollisionBack" type="CollisionShape3D" parent="ContainerPhysics"]
1555
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 8, -2.2)
1556
- shape = SubResource("BoxShape3D_back")
1557
-
1558
- [node name="CollisionFront" type="CollisionShape3D" parent="ContainerPhysics"]
1559
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 8, 2.2)
1560
- shape = SubResource("BoxShape3D_back")
1561
-
1562
- [node name="ContainerVisual" type="Node3D" parent="."]
1563
-
1564
- [node name="FruitsContainer" type="Node3D" parent="."]
1565
-
1566
- [node name="AimLine" type="MeshInstance3D" parent="."]
1567
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 7.5, 0)
1568
- mesh = SubResource("CylinderMesh_aim")
1569
-
1570
- [node name="Dropper" type="Node3D" parent="."]
1571
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 15.2, 0)
1572
-
1573
- [node name="CloudMesh" type="MeshInstance3D" parent="Dropper"]
1574
- mesh = SubResource("SphereMesh_cloud")
1575
-
1576
- [node name="PointerMesh" type="MeshInstance3D" parent="Dropper"]
1577
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.9, 0)
1578
- mesh = SubResource("CylinderMesh_pointer")
1579
-
1580
- [node name="UI" type="Control" parent="."]
1581
- layout_mode = 3
1582
- anchors_preset = 15
1583
- anchor_right = 1.0
1584
- anchor_bottom = 1.0
1585
- grow_horizontal = 2
1586
- grow_vertical = 2
1587
- mouse_filter = 2
1588
- script = ExtResource("2_ui")
1589
-
1590
- [node name="ScoreContainer" type="VBoxContainer" parent="UI"]
1591
- layout_mode = 0
1592
- offset_left = 30.0
1593
- offset_top = 25.0
1594
- offset_right = 260.0
1595
- offset_bottom = 110.0
1596
-
1597
- [node name="ScoreLabel" type="Label" parent="UI/ScoreContainer"]
1598
- layout_mode = 2
1599
- theme_override_font_sizes/font_size = 18
1600
- theme_override_colors/font_color = Color(0.85, 0.9, 1, 0.8)
1601
- text = "SCORE"
1602
-
1603
- [node name="ScoreValue" type="Label" parent="UI/ScoreContainer"]
1604
- layout_mode = 2
1605
- theme_override_font_sizes/font_size = 40
1606
- theme_override_colors/font_color = Color(1, 1, 1, 1)
1607
- text = "0"
1608
-
1609
- [node name="HighScoreValue" type="Label" parent="UI/ScoreContainer"]
1610
- layout_mode = 2
1611
- theme_override_font_sizes/font_size = 16
1612
- theme_override_colors/font_color = Color(1, 0.85, 0.3, 0.9)
1613
- text = "BEST: 0"
1614
-
1615
- [node name="NextContainer" type="VBoxContainer" parent="UI"]
1616
- layout_mode = 1
1617
- anchors_preset = 1
1618
- anchor_left = 1.0
1619
- anchor_right = 1.0
1620
- offset_left = -150.0
1621
- offset_top = 25.0
1622
- offset_right = -30.0
1623
- offset_bottom = 135.0
1624
- grow_horizontal = 0
1625
- alignment = 1
1626
-
1627
- [node name="NextTitle" type="Label" parent="UI/NextContainer"]
1628
- layout_mode = 2
1629
- theme_override_font_sizes/font_size = 16
1630
- theme_override_colors/font_color = Color(0.85, 0.9, 1, 0.8)
1631
- text = "NEXT"
1632
- horizontal_alignment = 1
1633
-
1634
- [node name="PreviewRect" type="ColorRect" parent="UI/NextContainer"]
1635
- custom_minimum_size = Vector2(48, 48)
1636
- layout_mode = 2
1637
- color = Color(0.95, 0.12, 0.28, 1)
1638
-
1639
- [node name="NextFruitName" type="Label" parent="UI/NextContainer"]
1640
- layout_mode = 2
1641
- theme_override_font_sizes/font_size = 14
1642
- theme_override_colors/font_color = Color(1, 1, 1, 0.9)
1643
- text = "Strawberry"
1644
- horizontal_alignment = 1
1645
-
1646
- [node name="EvolutionGuide" type="Control" parent="UI"]
1647
- layout_mode = 1
1648
- anchors_preset = 12
1649
- anchor_top = 1.0
1650
- anchor_right = 1.0
1651
- anchor_bottom = 1.0
1652
- offset_left = 20.0
1653
- offset_top = -65.0
1654
- offset_right = -20.0
1655
- offset_bottom = -15.0
1656
- grow_horizontal = 2
1657
- grow_vertical = 0
1658
- mouse_filter = 2
1659
- script = ExtResource("3_evo")
1660
-
1661
- [node name="DangerIndicator" type="ColorRect" parent="UI"]
1662
- visible = false
1663
- layout_mode = 1
1664
- anchors_preset = 10
1665
- anchor_right = 1.0
1666
- offset_top = 145.0
1667
- offset_bottom = 155.0
1668
- grow_horizontal = 2
1669
- color = Color(1, 0.15, 0.15, 0.6)
1670
-
1671
- [node name="GameOverPanel" type="Panel" parent="UI"]
1672
- visible = false
1673
- layout_mode = 1
1674
- anchors_preset = 8
1675
- anchor_left = 0.5
1676
- anchor_top = 0.5
1677
- anchor_right = 0.5
1678
- anchor_bottom = 0.5
1679
- offset_left = -170.0
1680
- offset_top = -140.0
1681
- offset_right = 170.0
1682
- offset_bottom = 140.0
1683
- grow_horizontal = 2
1684
- grow_vertical = 2
1685
-
1686
- [node name="Title" type="Label" parent="UI/GameOverPanel"]
1687
- layout_mode = 1
1688
- anchors_preset = 10
1689
- anchor_right = 1.0
1690
- offset_top = 25.0
1691
- offset_bottom = 65.0
1692
- grow_horizontal = 2
1693
- theme_override_font_sizes/font_size = 28
1694
- theme_override_colors/font_color = Color(1, 0.3, 0.3, 1)
1695
- text = "GAME OVER"
1696
- horizontal_alignment = 1
1697
-
1698
- [node name="FinalScore" type="Label" parent="UI/GameOverPanel"]
1699
- layout_mode = 1
1700
- anchors_preset = 10
1701
- anchor_right = 1.0
1702
- offset_top = 80.0
1703
- offset_bottom = 120.0
1704
- grow_horizontal = 2
1705
- theme_override_font_sizes/font_size = 22
1706
- text = "FINAL SCORE: 0"
1707
- horizontal_alignment = 1
1708
-
1709
- [node name="RestartButton" type="Button" parent="UI/GameOverPanel"]
1710
- layout_mode = 1
1711
- anchors_preset = 7
1712
- anchor_left = 0.5
1713
- anchor_top = 1.0
1714
- anchor_right = 0.5
1715
- anchor_bottom = 1.0
1716
- offset_left = -90.0
1717
- offset_top = -75.0
1718
- offset_right = 90.0
1719
- offset_bottom = -25.0
1720
- grow_horizontal = 2
1721
- grow_vertical = 0
1722
- theme_override_font_sizes/font_size = 20
1723
- text = "PLAY AGAIN"
1724
- `,
1725
- );
1726
-
1727
- // ==========================================
1728
- // 5. DOCUMENTATION (README.md)
1729
- // ==========================================
1730
-
1731
- writeFile(
1732
- "README.md",
1733
- `# Fruit Fusion 3D (Suika Game Mechanic in Godot 4)
1734
-
1735
- A popular merge puzzle game inspired by the Watermelon / Suika Game, implemented in **Godot 4** with procedural **OpenSCAD 3D models** and PBR materials.
1736
-
1737
- ## Game Overview
1738
- Drop different fruits into the transparent glass container. When two identical fruits collide, they merge with a satisfying pop and evolve into a larger, higher-tier fruit!
1739
-
1740
- ### Evolution Chain:
1741
- 1. 🍒 **Cherry**
1742
- 2. 🍓 **Strawberry**
1743
- 3. 🍇 **Grape**
1744
- 4. 🍊 **Tangerine / Orange**
1745
- 5. 🍎 **Apple**
1746
- 6. 🍑 **Peach**
1747
- 7. 🍈 **Melon**
1748
- 8. 🍉 **Watermelon**
1749
- 9. 👑 **King Sun** (Ultimate crowned glowing celestial watermelon)
1750
-
1751
- ---
1752
-
1753
- ## Controls
1754
- - **Aim Dropper**:
1755
- - Touch & Drag along the glass container (Mobile / Web)
1756
- - Mouse Move / Click & Drag along the glass container (Desktop)
1757
- - Left / Right Arrow keys / A & D keys
1758
- - **Drop Fruit**:
1759
- - Release Touch / Release Left Click
1760
- - Spacebar
1761
- - Enter
1762
- - Down Arrow
1763
- `,
1764
- );
1765
-
1766
- console.log(`
1767
- =====================================================
1768
- Setup complete! Project created in ./${PROJECT_DIR}
1769
- To run the game:
1770
- 1. Open Godot 4
1771
- 2. Import the project located at ./${PROJECT_DIR}/project.godot
1772
- 3. Run the project (F5)
1773
- =====================================================
1774
- `);