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.
- package/README.md +1 -5
- package/godot/README.md +0 -3
- package/package.json +1 -1
- package/godot/examples/LICENSE +0 -21
- package/godot/examples/README.md +0 -91
- package/godot/examples/fruit_fusion_3d.js +0 -1774
- package/godot/examples/horde_survivor_3d.js +0 -2753
- package/godot/examples/kids_digit_writing.js +0 -2532
- package/godot/examples/naval_battle_3d.js +0 -1691
- package/godot/examples/package.json +0 -5
- package/godot/export_presets.cfg +0 -25
|
@@ -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
|
-
`);
|