scad-gltf 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1691 +0,0 @@
1
- // Self-contained generator for Battleship 3D with the original flame spire style elevated higher,
2
- // corrected HUD reticle corners, and full English localization.
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const ROOT_DIR = "naval_battle_3d";
7
-
8
- function ensureDir(dirPath) {
9
- if (!fs.existsSync(dirPath)) {
10
- fs.mkdirSync(dirPath, { recursive: true });
11
- }
12
- }
13
-
14
- function writeFile(relativePath, content) {
15
- const fullPath = path.join(ROOT_DIR, relativePath);
16
- ensureDir(path.dirname(fullPath));
17
- fs.writeFileSync(fullPath, content.trimStart(), "utf8");
18
- console.log(`Created: ${relativePath}`);
19
- }
20
-
21
- console.log(`\n=== Generating Battleship 3D: ${ROOT_DIR} ===\n`);
22
- ensureDir(ROOT_DIR);
23
-
24
- // =========================================================================
25
- // 1. ADDON: SCAD IMPORTER
26
- // =========================================================================
27
-
28
- writeFile(
29
- "addons/scad_importer/plugin.cfg",
30
- `[plugin]
31
-
32
- name="OpenSCAD GLTF Importer"
33
- description="Imports .scad files directly as 3D scenes using scad-gltf"
34
- author="Ilia Grigorev"
35
- version="0.1"
36
- script="scad_plugin.gd"
37
- `,
38
- );
39
-
40
- writeFile(
41
- "addons/scad_importer/scad_plugin.gd",
42
- `@tool
43
- extends EditorPlugin
44
-
45
- var import_plugin
46
-
47
- func _enter_tree():
48
- import_plugin = preload("res://addons/scad_importer/scad_importer.gd").new()
49
- add_scene_format_importer_plugin(import_plugin)
50
-
51
- func _exit_tree():
52
- remove_scene_format_importer_plugin(import_plugin)
53
- import_plugin = null
54
- `,
55
- );
56
-
57
- writeFile(
58
- "addons/scad_importer/scad_importer.gd",
59
- `@tool
60
- extends EditorSceneFormatImporter
61
-
62
- func _get_extensions():
63
- return PackedStringArray(["scad"])
64
-
65
- func _get_import_flags():
66
- return EditorSceneFormatImporter.IMPORT_SCENE
67
-
68
- func _import_scene(path: String, flags: int, options: Dictionary) -> Object:
69
- var global_source = ProjectSettings.globalize_path(path)
70
- var unique_id = str(hash(path))
71
- var temp_glb_path = ProjectSettings.globalize_path("user://scad_cache_" + unique_id + ".glb")
72
-
73
- var args = PackedStringArray()
74
- args.append(global_source)
75
- args.append(temp_glb_path)
76
-
77
- var output = []
78
- print("Importing %s via scad-convert..." % path.get_file())
79
-
80
- var exit_code = -1
81
- if OS.get_name() == "Windows":
82
- var win_args = PackedStringArray(["/c", "scad-convert"])
83
- win_args.append_array(args)
84
- exit_code = OS.execute("cmd.exe", win_args, output, true)
85
- else:
86
- exit_code = OS.execute("scad-convert", args, output, true)
87
-
88
- if exit_code != 0:
89
- print("scad-convert conversion failed for %s. Attempting fallback to local scad-serve..." % path.get_file())
90
- var fallback_success = _try_scad_serve_fallback(global_source, temp_glb_path)
91
-
92
- if not fallback_success:
93
- push_error("Failed to compile SCAD file: %s. Ensure Node.js is installed or scad-serve is running." % path.get_file())
94
- push_error("scad-convert output: ", "\\n".join(output))
95
- return null
96
-
97
- var gltf_doc = GLTFDocument.new()
98
- var gltf_state = GLTFState.new()
99
- var err = gltf_doc.append_from_file(temp_glb_path, gltf_state)
100
-
101
- if FileAccess.file_exists(temp_glb_path):
102
- DirAccess.remove_absolute(temp_glb_path)
103
-
104
- if err != OK:
105
- push_error("Failed to parse the generated GLB for %s." % path.get_file())
106
- return null
107
-
108
- var generated_scene = gltf_doc.generate_scene(gltf_state)
109
- if generated_scene:
110
- generated_scene.name = path.get_file().get_basename()
111
-
112
- return generated_scene
113
-
114
- func _get_relative_path(base: String, target: String) -> String:
115
- var base_parts = base.replace("\\\\", "/").split("/", false)
116
- var target_parts = target.replace("\\\\", "/").split("/", false)
117
-
118
- if OS.get_name() == "Windows":
119
- if base_parts.size() > 0 and target_parts.size() > 0:
120
- if base_parts[0].nocasecmp_to(target_parts[0]) != 0:
121
- return target
122
-
123
- var common_count = 0
124
- var min_len = min(base_parts.size(), target_parts.size())
125
- for i in range(min_len):
126
- if base_parts[i].nocasecmp_to(target_parts[i]) == 0:
127
- common_count += 1
128
- else:
129
- break
130
-
131
- var rel_parts = PackedStringArray()
132
- for i in range(common_count, base_parts.size()):
133
- rel_parts.append("..")
134
-
135
- for i in range(common_count, target_parts.size()):
136
- rel_parts.append(target_parts[i])
137
-
138
- return "/".join(rel_parts)
139
-
140
- func _get_dependencies_recursive(file_path: String, visited: Dictionary) -> void:
141
- if visited.has(file_path):
142
- return
143
-
144
- visited[file_path] = ""
145
-
146
- if not FileAccess.file_exists(file_path):
147
- return
148
-
149
- var file = FileAccess.open(file_path, FileAccess.READ)
150
- if not file:
151
- return
152
-
153
- var content = file.get_as_text()
154
- file.close()
155
-
156
- visited[file_path] = content
157
-
158
- var regex = RegEx.new()
159
- regex.compile("(?:include|use)\\\\s*[<\\\"]([^>\\\"]+)[>\\\"]")
160
-
161
- var base_dir = file_path.get_base_dir()
162
- for result in regex.search_all(content):
163
- var dep_rel_path = result.get_string(1)
164
- var dep_abs_path = base_dir.path_join(dep_rel_path).simplify_path()
165
- _get_dependencies_recursive(dep_abs_path, visited)
166
-
167
- func _get_dependencies(file_path: String) -> Dictionary:
168
- var visited = {}
169
- _get_dependencies_recursive(file_path, visited)
170
- return visited
171
-
172
- func _try_scad_serve_fallback(source_path: String, out_glb_path: String) -> bool:
173
- var deps = _get_dependencies(source_path)
174
- var content = deps.get(source_path, "")
175
- deps.erase(source_path)
176
-
177
- if content == "":
178
- return false
179
-
180
- var additional_files = {}
181
- var base_dir = source_path.get_base_dir()
182
- for dep_path in deps.keys():
183
- var rel_path = _get_relative_path(base_dir, dep_path)
184
- additional_files[rel_path] = deps[dep_path]
185
-
186
- var http = HTTPClient.new()
187
- var err = http.connect_to_host("127.0.0.1", 3000)
188
- if err != OK:
189
- return false
190
-
191
- var max_wait = 500
192
- var wait = 0
193
- while http.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]:
194
- http.poll()
195
- OS.delay_msec(10)
196
- wait += 1
197
- if wait > max_wait:
198
- return false
199
-
200
- if http.get_status() != HTTPClient.STATUS_CONNECTED:
201
- return false
202
-
203
- var headers = PackedStringArray(["Content-Type: application/json"])
204
-
205
- var payload = {
206
- "content": content,
207
- "options": {
208
- "additionalFiles": additional_files
209
- }
210
- }
211
-
212
- var body = JSON.stringify(payload)
213
- err = http.request(HTTPClient.METHOD_POST, "/api/convert", headers, body)
214
- if err != OK:
215
- return false
216
-
217
- max_wait = 6000
218
- wait = 0
219
- while http.get_status() == HTTPClient.STATUS_REQUESTING:
220
- http.poll()
221
- OS.delay_msec(10)
222
- wait += 1
223
- if wait > max_wait:
224
- return false
225
-
226
- if http.has_response() and http.get_response_code() == 200:
227
- var rb = PackedByteArray()
228
- while http.get_status() == HTTPClient.STATUS_BODY:
229
- http.poll()
230
- var chunk = http.read_response_body_chunk()
231
- if chunk.size() == 0:
232
- OS.delay_msec(10)
233
- else:
234
- rb.append_array(chunk)
235
-
236
- if rb.is_empty():
237
- return false
238
-
239
- var out_file = FileAccess.open(out_glb_path, FileAccess.WRITE)
240
- if not out_file:
241
- return false
242
- out_file.store_buffer(rb)
243
- out_file.close()
244
-
245
- print("Successfully compiled %s using scad-serve fallback." % source_path.get_file())
246
- return true
247
-
248
- return false
249
- `,
250
- );
251
-
252
- // =========================================================================
253
- // 2. OPENSCAD PROCEDURAL 3D ASSETS (.scad)
254
- // =========================================================================
255
-
256
- // marker_hit.scad - Original glowing crystal flame spire style, elevated higher (~2.8m)
257
- writeFile(
258
- "assets/models/marker_hit.scad",
259
- `// Hit Marker: Flaming Crimson Naval Beacon (Elevated Flame Spires)
260
- $fn = 20;
261
-
262
- module hit_marker() {
263
- // Buoy Base ring
264
- color([0.15, 0.15, 0.18], metalness=0.8, roughness=0.4, $asa=30) {
265
- cylinder(h=0.2, r=0.6, center=true);
266
- }
267
- // Warning red float
268
- color([0.9, 0.1, 0.1], metalness=0.3, roughness=0.3, $asa=30) {
269
- translate([0, 0, 0.28])
270
- cylinder(h=0.36, r1=0.55, r2=0.4, center=true);
271
- }
272
- // Burning core / high energy plasma flame spire (elongated upwards)
273
- color([1.0, 0.2, 0.05], metalness=0.1, roughness=0.1, emissive=[1.0, 0.25, 0.0], emissiveIntensity=3.2, $asa=45) {
274
- // Main central flame column (rises up to Z=2.8m)
275
- translate([0, 0, 1.45])
276
- cylinder(h=2.1, r1=0.26, r2=0.02, center=true);
277
-
278
- // Lower tier flame crystals
279
- for (a = [0, 60, 120, 180, 240, 300]) {
280
- rotate([0, 0, a]) translate([0.2, 0, 1.0])
281
- cylinder(h=1.2, r1=0.08, r2=0.01, center=true);
282
- }
283
-
284
- // Upper tier flame crystals
285
- for (a = [30, 90, 150, 210, 270, 330]) {
286
- rotate([0, 0, a]) translate([0.12, 0, 1.75])
287
- cylinder(h=1.1, r1=0.06, r2=0.01, center=true);
288
- }
289
- }
290
- // Pulsing inner diamond core
291
- color([1.0, 0.9, 0.2], metalness=0.1, roughness=0.1, emissive=[1.0, 0.9, 0.2], emissiveIntensity=4.5) {
292
- translate([0, 0, 0.65])
293
- sphere(r=0.25);
294
- }
295
- }
296
-
297
- hit_marker();
298
- `,
299
- );
300
-
301
- // targeting_reticle.scad - Corrected HUD framing corners pointing inward
302
- writeFile(
303
- "assets/models/targeting_reticle.scad",
304
- `// Holographic 3D Targeting Reticle
305
- // Fits standard 2.0m grid tile (size 1.8x1.8m)
306
- $fn = 16;
307
-
308
- module corner_bracket() {
309
- // Top-right corner apex is at (+0.9, +0.9)
310
- // Horizontal arm goes inward along -X to 0.5
311
- translate([0.7, 0.9, 0.08])
312
- cube([0.4, 0.09, 0.12], center=true);
313
- // Vertical arm goes inward along -Y to 0.5
314
- translate([0.9, 0.7, 0.08])
315
- cube([0.09, 0.4, 0.12], center=true);
316
- // Vertical corner indicator post
317
- translate([0.9, 0.9, 0.2])
318
- cube([0.09, 0.09, 0.35], center=true);
319
- }
320
-
321
- module reticle() {
322
- color([0.1, 0.95, 1.0], metalness=0.2, roughness=0.2, emissive=[0.15, 0.95, 1.0], emissiveIntensity=3.2) {
323
- // 4 inward-framing brackets: 0=TR, 90=TL, 180=BL, 270=BR
324
- for (rot = [0, 90, 180, 270]) {
325
- rotate([0, 0, rot])
326
- corner_bracket();
327
- }
328
- // Center crosshair ring and dot
329
- cylinder(h=0.08, r=0.12, center=true);
330
- difference() {
331
- cylinder(h=0.06, r=0.36, center=true);
332
- cylinder(h=0.09, r=0.30, center=true);
333
- }
334
- }
335
- }
336
-
337
- reticle();
338
- `,
339
- );
340
-
341
- // marker_miss.scad - Water splash buoy
342
- writeFile(
343
- "assets/models/marker_miss.scad",
344
- `// Miss Marker: White Water Splash & Marine Buoy
345
- $fn = 20;
346
-
347
- module miss_marker() {
348
- color([0.2, 0.7, 0.9], metalness=0.1, roughness=0.2, emissive=[0.1, 0.4, 0.6], emissiveIntensity=1.0, $asa=30) {
349
- difference() {
350
- cylinder(h=0.1, r=0.75, center=true);
351
- cylinder(h=0.15, r=0.5, center=true);
352
- }
353
- }
354
- color([0.9, 0.92, 0.95], metalness=0.2, roughness=0.5, $asa=30) {
355
- translate([0, 0, 0.3])
356
- cylinder(h=0.4, r1=0.4, r2=0.3, center=true);
357
- }
358
- color([0.1, 0.6, 1.0], metalness=0.1, roughness=0.1, emissive=[0.2, 0.7, 1.0], emissiveIntensity=2.0, $asa=30) {
359
- translate([0, 0, 0.55])
360
- sphere(r=0.25);
361
- }
362
- color([0.95, 0.95, 0.95], metalness=0.8, roughness=0.3) {
363
- translate([0, 0, 0.9])
364
- cylinder(h=0.6, r=0.02, center=true);
365
- }
366
- }
367
-
368
- miss_marker();
369
- `,
370
- );
371
-
372
- // ocean_grid_table.scad - 10x10 Tactical Ocean Board
373
- writeFile(
374
- "assets/models/ocean_grid_table.scad",
375
- `// 10x10 Tactical Ocean Grid Table
376
- $fn = 24;
377
-
378
- module wooden_border() {
379
- color([0.14, 0.09, 0.05], metalness=0.1, roughness=0.7, $asa=30) {
380
- difference() {
381
- translate([0, 0, -0.4])
382
- cube([22.0, 22.0, 0.8], center=true);
383
- translate([0, 0, 0.1])
384
- cube([20.0, 20.0, 1.0], center=true);
385
- }
386
- }
387
- color([0.85, 0.65, 0.18], metalness=0.9, roughness=0.25, $asa=45) {
388
- for (mx = [-10.5, 10.5]) {
389
- for (my = [-10.5, 10.5]) {
390
- translate([mx, my, 0.05])
391
- cube([1.5, 1.5, 0.22], center=true);
392
- }
393
- }
394
- }
395
- }
396
-
397
- module water_surface() {
398
- color([0.04, 0.16, 0.32], metalness=0.2, roughness=0.15, emissive=[0.01, 0.04, 0.08], emissiveIntensity=1.0, $asa=30) {
399
- translate([0, 0, -0.1])
400
- cube([20.0, 20.0, 0.3], center=true);
401
- }
402
- }
403
-
404
- module grid_lines() {
405
- color([0.2, 0.65, 0.9], metalness=0.3, roughness=0.3, emissive=[0.12, 0.55, 0.85], emissiveIntensity=1.8) {
406
- for (i = [-5 : 5]) {
407
- translate([i * 2.0, 0, 0.06])
408
- cube([0.05, 20.0, 0.04], center=true);
409
- translate([0, i * 2.0, 0.06])
410
- cube([20.0, 0.05, 0.04], center=true);
411
- }
412
- }
413
- }
414
-
415
- union() {
416
- wooden_border();
417
- water_surface();
418
- grid_lines();
419
- }
420
- `,
421
- );
422
-
423
- // ship_carrier.scad - 5 cells (~9.2m long)
424
- writeFile(
425
- "assets/models/ship_carrier.scad",
426
- `// Aircraft Carrier (Size: 5 Cells = ~9.2m)
427
- $fn = 20;
428
-
429
- anim = [
430
- ["RadarSpin", [
431
- ["RadarTower", [
432
- [0.0, [0, 0, 0]],
433
- [1.0, [0, 0, 90]],
434
- [2.0, [0, 0, 180]],
435
- [3.0, [0, 0, 270]],
436
- [4.0, [0, 0, 360]]
437
- ]]
438
- ]]
439
- ];
440
-
441
- module carrier_hull() {
442
- color([0.22, 0.25, 0.28], metalness=0.8, roughness=0.45, $asa=35) {
443
- hull() {
444
- translate([0, -4.4, 0.4]) cube([1.6, 0.4, 0.7], center=true);
445
- translate([0, 0.0, 0.4]) cube([1.7, 5.0, 0.7], center=true);
446
- translate([0, 4.3, 0.4]) cube([0.4, 0.2, 0.7], center=true);
447
- }
448
- }
449
- color([0.15, 0.16, 0.18], metalness=0.3, roughness=0.7, $asa=20) {
450
- translate([-0.1, 0, 0.85])
451
- cube([1.9, 9.2, 0.2], center=true);
452
- }
453
- color([0.9, 0.75, 0.1], metalness=0.1, roughness=0.5, emissive=[0.4, 0.3, 0.0], emissiveIntensity=1.0) {
454
- translate([-0.2, 0, 0.96])
455
- cube([0.1, 8.4, 0.03], center=true);
456
- for (y = [-3.0 : 1.5 : 3.0]) {
457
- translate([-0.2, y, 0.96])
458
- cube([0.8, 0.15, 0.03], center=true);
459
- }
460
- }
461
- color([0.3, 0.33, 0.36], metalness=0.7, roughness=0.4, $asa=30) {
462
- translate([0.65, 0.4, 1.25])
463
- cube([0.45, 1.8, 0.7], center=true);
464
- translate([0.65, 0.8, 1.7])
465
- cube([0.35, 0.8, 0.4], center=true);
466
- color([0.2, 0.8, 0.9], metalness=0.9, roughness=0.1, emissive=[0.2, 0.8, 0.9], emissiveIntensity=1.2) {
467
- translate([0.65, 1.15, 1.7])
468
- cube([0.3, 0.12, 0.15], center=true);
469
- }
470
- }
471
- }
472
-
473
- armature(animations=anim) {
474
- carrier_hull();
475
- bone(name="RadarTower", t=[0.65, 0.4, 1.95], r=[0, 0, 0]) {
476
- color([0.85, 0.85, 0.1], metalness=0.8, roughness=0.3) {
477
- cylinder(h=0.4, r=0.04, center=true);
478
- translate([0, 0, 0.2])
479
- cube([0.6, 0.08, 0.15], center=true);
480
- }
481
- }
482
- }
483
- `,
484
- );
485
-
486
- // ship_battleship.scad - 4 cells (~7.2m long)
487
- writeFile(
488
- "assets/models/ship_battleship.scad",
489
- `// Battleship (Size: 4 Cells = ~7.2m)
490
- $fn = 20;
491
-
492
- anim = [
493
- ["IdleTurret", [
494
- ["ForwardTurret", [
495
- [0.0, [0, 0, 0]],
496
- [1.5, [0, 0, 15]],
497
- [3.0, [0, 0, 0]],
498
- [4.5, [0, 0, -15]],
499
- [6.0, [0, 0, 0]]
500
- ]]
501
- ]]
502
- ];
503
-
504
- module main_hull() {
505
- color([0.25, 0.28, 0.32], metalness=0.85, roughness=0.35, $asa=35) {
506
- hull() {
507
- translate([0, -3.4, 0.4]) cube([1.4, 0.4, 0.75], center=true);
508
- translate([0, 0.0, 0.4]) cube([1.55, 3.5, 0.8], center=true);
509
- translate([0, 3.4, 0.4]) cube([0.3, 0.2, 0.75], center=true);
510
- }
511
- }
512
- color([0.2, 0.22, 0.25], metalness=0.6, roughness=0.5, $asa=30) {
513
- translate([0, 0.0, 0.85])
514
- cube([1.1, 3.2, 0.2], center=true);
515
- translate([0, 0.3, 1.25])
516
- cube([0.7, 1.4, 0.6], center=true);
517
- translate([0, -0.7, 1.3])
518
- cylinder(h=0.7, r1=0.22, r2=0.18, center=true);
519
- translate([0, -1.2, 1.25])
520
- cylinder(h=0.6, r1=0.2, r2=0.16, center=true);
521
- }
522
- color([0.1, 0.8, 0.7], metalness=0.5, roughness=0.2, emissive=[0.1, 0.8, 0.7], emissiveIntensity=1.5) {
523
- translate([0, 0.95, 1.35])
524
- cube([0.6, 0.12, 0.12], center=true);
525
- }
526
- color([0.32, 0.35, 0.4], metalness=0.9, roughness=0.3, $asa=40) {
527
- translate([0, -2.2, 0.9]) {
528
- cylinder(h=0.35, r=0.45, center=true);
529
- translate([0.1, -0.6, 0.05]) rotate([90, 0, 0]) cylinder(h=0.9, r=0.07, center=true);
530
- translate([-0.1, -0.6, 0.05]) rotate([90, 0, 0]) cylinder(h=0.9, r=0.07, center=true);
531
- }
532
- }
533
- }
534
-
535
- armature(animations=anim) {
536
- main_hull();
537
- bone(name="ForwardTurret", t=[0, 1.8, 0.9], r=[0, 0, 0]) {
538
- color([0.35, 0.38, 0.44], metalness=0.9, roughness=0.3, $asa=40) {
539
- cylinder(h=0.38, r=0.48, center=true);
540
- translate([0.12, 0.7, 0.06]) rotate([90, 0, 0]) cylinder(h=1.0, r=0.075, center=true);
541
- translate([-0.12, 0.7, 0.06]) rotate([90, 0, 0]) cylinder(h=1.0, r=0.075, center=true);
542
- }
543
- }
544
- }
545
- `,
546
- );
547
-
548
- // ship_cruiser.scad - 3 cells (~5.2m long)
549
- writeFile(
550
- "assets/models/ship_cruiser.scad",
551
- `// Heavy Cruiser (Size: 3 Cells = ~5.2m)
552
- $fn = 18;
553
-
554
- module cruiser() {
555
- color([0.28, 0.32, 0.36], metalness=0.8, roughness=0.4, $asa=35) {
556
- hull() {
557
- translate([0, -2.4, 0.35]) cube([1.2, 0.3, 0.7], center=true);
558
- translate([0, 0.0, 0.35]) cube([1.3, 2.5, 0.7], center=true);
559
- translate([0, 2.4, 0.35]) cube([0.25, 0.2, 0.7], center=true);
560
- }
561
- }
562
- color([0.22, 0.24, 0.28], metalness=0.6, roughness=0.5, $asa=30) {
563
- translate([0, 0.2, 0.95])
564
- cube([0.75, 1.8, 0.55], center=true);
565
- translate([0, -0.8, 1.05])
566
- cylinder(h=0.6, r=0.2, center=true);
567
- }
568
- color([0.4, 0.44, 0.48], metalness=0.9, roughness=0.3, $asa=35) {
569
- translate([0, 1.5, 0.85]) {
570
- cylinder(h=0.3, r=0.35, center=true);
571
- translate([0, 0.55, 0.05]) rotate([90, 0, 0]) cylinder(h=0.8, r=0.06, center=true);
572
- }
573
- translate([0, -1.6, 0.85]) {
574
- cube([0.6, 0.6, 0.25], center=true);
575
- translate([0, 0, 0.25]) sphere(r=0.25);
576
- }
577
- }
578
- color([0.0, 0.9, 0.8], metalness=0.4, roughness=0.2, emissive=[0.0, 0.9, 0.8], emissiveIntensity=1.4) {
579
- translate([0, 0.95, 1.05])
580
- cube([0.55, 0.1, 0.1], center=true);
581
- }
582
- }
583
-
584
- cruiser();
585
- `,
586
- );
587
-
588
- // ship_destroyer.scad - 2 cells (~3.4m long)
589
- writeFile(
590
- "assets/models/ship_destroyer.scad",
591
- `// Destroyer (Size: 2 Cells = ~3.4m)
592
- $fn = 16;
593
-
594
- module destroyer() {
595
- color([0.25, 0.3, 0.34], metalness=0.85, roughness=0.35, $asa=35) {
596
- hull() {
597
- translate([0, -1.5, 0.3]) cube([1.0, 0.3, 0.6], center=true);
598
- translate([0, 0.0, 0.3]) cube([1.05, 1.6, 0.6], center=true);
599
- translate([0, 1.5, 0.3]) cube([0.2, 0.1, 0.6], center=true);
600
- }
601
- }
602
- color([0.2, 0.23, 0.26], metalness=0.5, roughness=0.5, $asa=30) {
603
- translate([0, 0.1, 0.8])
604
- cube([0.65, 1.0, 0.45], center=true);
605
- translate([0, -0.5, 0.85])
606
- cylinder(h=0.45, r=0.15, center=true);
607
- }
608
- color([0.45, 0.48, 0.52], metalness=0.9, roughness=0.25, $asa=40) {
609
- translate([0, 0.95, 0.72]) {
610
- cylinder(h=0.24, r=0.28, center=true);
611
- translate([0, 0.4, 0.04]) rotate([90, 0, 0]) cylinder(h=0.6, r=0.05, center=true);
612
- }
613
- }
614
- color([0.15, 0.15, 0.15], metalness=0.8, roughness=0.4) {
615
- translate([0, -1.0, 0.68]) {
616
- rotate([0, 0, 90]) {
617
- translate([0.08, 0, 0]) cylinder(h=0.5, r=0.06, center=true);
618
- translate([-0.08, 0, 0]) cylinder(h=0.5, r=0.06, center=true);
619
- }
620
- }
621
- }
622
- color([0.0, 1.0, 0.6], metalness=0.2, roughness=0.2, emissive=[0.0, 1.0, 0.6], emissiveIntensity=1.6) {
623
- translate([0, 0.5, 0.85])
624
- cube([0.5, 0.08, 0.08], center=true);
625
- }
626
- }
627
-
628
- destroyer();
629
- `,
630
- );
631
-
632
- // ship_patrol.scad - 1 cell (~1.5m long)
633
- writeFile(
634
- "assets/models/ship_patrol.scad",
635
- `// Patrol Boat (Size: 1 Cell = ~1.5m)
636
- $fn = 16;
637
-
638
- module patrol_boat() {
639
- color([0.22, 0.28, 0.32], metalness=0.75, roughness=0.4, $asa=30) {
640
- hull() {
641
- translate([0, -0.65, 0.25]) cube([0.85, 0.2, 0.5], center=true);
642
- translate([0, 0.0, 0.25]) cube([0.9, 0.8, 0.5], center=true);
643
- translate([0, 0.65, 0.25]) cube([0.2, 0.1, 0.5], center=true);
644
- }
645
- }
646
- color([0.18, 0.2, 0.24], metalness=0.5, roughness=0.5, $asa=30) {
647
- translate([0, -0.05, 0.65])
648
- cube([0.55, 0.55, 0.35], center=true);
649
- }
650
- color([1.0, 0.6, 0.1], metalness=0.3, roughness=0.2, emissive=[1.0, 0.6, 0.1], emissiveIntensity=1.6) {
651
- translate([0, 0.2, 0.7])
652
- cube([0.45, 0.08, 0.12], center=true);
653
- }
654
- color([0.5, 0.55, 0.6], metalness=0.9, roughness=0.25) {
655
- translate([0, 0.45, 0.58]) {
656
- cylinder(h=0.18, r=0.16, center=true);
657
- translate([0, 0.22, 0.03]) rotate([90, 0, 0]) cylinder(h=0.35, r=0.035, center=true);
658
- }
659
- }
660
- }
661
-
662
- patrol_boat();
663
- `,
664
- );
665
-
666
- // artillery_shell.scad - Naval shell projectile
667
- writeFile(
668
- "assets/models/artillery_shell.scad",
669
- `// Ballistic Naval Artillery Shell
670
- $fn = 18;
671
-
672
- module shell() {
673
- color([0.85, 0.65, 0.2], metalness=0.9, roughness=0.3, $asa=45) {
674
- cylinder(h=0.5, r=0.12, center=true);
675
- translate([0, 0, 0.25])
676
- cylinder(h=0.35, r1=0.12, r2=0.01, center=true);
677
- }
678
- color([0.9, 0.45, 0.1], metalness=0.95, roughness=0.2) {
679
- translate([0, 0, -0.18])
680
- cylinder(h=0.08, r=0.125, center=true);
681
- }
682
- color([1.0, 0.6, 0.1], metalness=0.1, roughness=0.1, emissive=[1.0, 0.7, 0.1], emissiveIntensity=4.0) {
683
- translate([0, 0, -0.32])
684
- cylinder(h=0.2, r1=0.08, r2=0.01, center=true);
685
- }
686
- }
687
-
688
- rotate([-90, 0, 0])
689
- shell();
690
- `,
691
- );
692
-
693
- // =========================================================================
694
- // 3. GDSCRIPT LOGIC (ENGLISH)
695
- // =========================================================================
696
-
697
- writeFile(
698
- "scripts/grid_manager.gd",
699
- `class_name GridManager
700
- extends RefCounted
701
-
702
- const LETTERS: Array[String] = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]
703
-
704
- const FLEET_DEFINITIONS: Array[Dictionary] = [
705
- {"name": "Battleship (4)", "size": 4, "scad": "res://assets/models/ship_battleship.scad"},
706
- {"name": "Cruiser (3)", "size": 3, "scad": "res://assets/models/ship_cruiser.scad"},
707
- {"name": "Cruiser (3)", "size": 3, "scad": "res://assets/models/ship_cruiser.scad"},
708
- {"name": "Destroyer (2)", "size": 2, "scad": "res://assets/models/ship_destroyer.scad"},
709
- {"name": "Destroyer (2)", "size": 2, "scad": "res://assets/models/ship_destroyer.scad"},
710
- {"name": "Destroyer (2)", "size": 2, "scad": "res://assets/models/ship_destroyer.scad"},
711
- {"name": "Patrol Boat (1)", "size": 1, "scad": "res://assets/models/ship_patrol.scad"},
712
- {"name": "Patrol Boat (1)", "size": 1, "scad": "res://assets/models/ship_patrol.scad"},
713
- {"name": "Patrol Boat (1)", "size": 1, "scad": "res://assets/models/ship_patrol.scad"},
714
- {"name": "Patrol Boat (1)", "size": 1, "scad": "res://assets/models/ship_patrol.scad"}
715
- ]
716
-
717
- const GRID_SIZE = 10
718
- const CELL_WORLD_SIZE = 2.0
719
-
720
- class ShipData:
721
- var id: int
722
- var name: String
723
- var size: int
724
- var origin_x: int
725
- var origin_y: int
726
- var horizontal: bool
727
- var hits: int = 0
728
- var scad_path: String
729
- var node_ref: Node3D = null
730
-
731
- func is_sunk() -> bool:
732
- return hits >= size
733
-
734
- func get_occupied_cells() -> Array[Vector2i]:
735
- var cells: Array[Vector2i] = []
736
- for i in range(size):
737
- if horizontal:
738
- cells.append(Vector2i(origin_x + i, origin_y))
739
- else:
740
- cells.append(Vector2i(origin_x, origin_y + i))
741
- return cells
742
-
743
- func get_surrounding_cells() -> Array[Vector2i]:
744
- var surround: Array[Vector2i] = []
745
- var occupied = get_occupied_cells()
746
- for c in occupied:
747
- for dx in [-1, 0, 1]:
748
- for dy in [-1, 0, 1]:
749
- var neighbor = Vector2i(c.x + dx, c.y + dy)
750
- if neighbor.x >= 0 and neighbor.x < GRID_SIZE and neighbor.y >= 0 and neighbor.y < GRID_SIZE:
751
- if not occupied.has(neighbor) and not surround.has(neighbor):
752
- surround.append(neighbor)
753
- return surround
754
-
755
- class BoardState:
756
- var shots: Dictionary = {}
757
- var ships: Array[ShipData] = []
758
- var cell_to_ship: Dictionary = {}
759
-
760
- func has_shot(x: int, y: int) -> bool:
761
- return shots.has(Vector2i(x, y))
762
-
763
- func get_all_sunk() -> bool:
764
- for s in ships:
765
- if not s.is_sunk():
766
- return false
767
- return ships.size() > 0
768
-
769
- func can_place_ship(ship_size: int, ox: int, oy: int, horizontal: bool) -> bool:
770
- for i in range(ship_size):
771
- var cx = ox + (i if horizontal else 0)
772
- var cy = oy + (0 if horizontal else i)
773
- if cx < 0 or cx >= GRID_SIZE or cy < 0 or cy >= GRID_SIZE:
774
- return false
775
- for dx in [-1, 0, 1]:
776
- for dy in [-1, 0, 1]:
777
- var test_pos = Vector2i(cx + dx, cy + dy)
778
- if cell_to_ship.has(test_pos):
779
- return false
780
- return true
781
-
782
- func add_ship(def: Dictionary, id: int, ox: int, oy: int, horizontal: bool) -> ShipData:
783
- var s = ShipData.new()
784
- s.id = id
785
- s.name = def["name"]
786
- s.size = def["size"]
787
- s.scad_path = def["scad"]
788
- s.origin_x = ox
789
- s.origin_y = oy
790
- s.horizontal = horizontal
791
- ships.append(s)
792
- for cell in s.get_occupied_cells():
793
- cell_to_ship[cell] = s
794
- return s
795
-
796
- func clear():
797
- shots.clear()
798
- ships.clear()
799
- cell_to_ship.clear()
800
-
801
- static func generate_random_fleet(board: BoardState):
802
- board.clear()
803
- var rng = RandomNumberGenerator.new()
804
- rng.randomize()
805
-
806
- var ship_id = 0
807
- for def in FLEET_DEFINITIONS:
808
- var placed = false
809
- var attempts = 0
810
- while not placed and attempts < 1000:
811
- attempts += 1
812
- var horiz = rng.randi() % 2 == 0
813
- var max_x = (GRID_SIZE - def["size"]) if horiz else (GRID_SIZE - 1)
814
- var max_y = (GRID_SIZE - 1) if horiz else (GRID_SIZE - def["size"])
815
- var rx = rng.randi_range(0, max_x)
816
- var ry = rng.randi_range(0, max_y)
817
- if board.can_place_ship(def["size"], rx, ry, horiz):
818
- board.add_ship(def, ship_id, rx, ry, horiz)
819
- placed = true
820
- ship_id += 1
821
- if not placed:
822
- generate_random_fleet(board)
823
- return
824
-
825
- static func grid_to_local_pos(x: int, y: int) -> Vector3:
826
- var local_x = (x - 4.5) * CELL_WORLD_SIZE
827
- var local_z = (y - 4.5) * CELL_WORLD_SIZE
828
- return Vector3(local_x, 0.1, local_z)
829
-
830
- static func get_cell_name(cell: Vector2i) -> String:
831
- var col = LETTERS[clampi(cell.x, 0, 9)]
832
- var row = str(cell.y + 1)
833
- return col + "-" + row
834
- `,
835
- );
836
-
837
- writeFile(
838
- "scripts/projectile.gd",
839
- `class_name ShellProjectile
840
- extends Node3D
841
-
842
- signal impacted(target_cell: Vector2i, is_hit: bool)
843
-
844
- var start_pos: Vector3
845
- var end_pos: Vector3
846
- var arc_height: float = 9.0
847
- var duration: float = 0.85
848
- var elapsed: float = 0.0
849
- var target_cell: Vector2i
850
- var is_hit_result: bool
851
-
852
- func launch(from_pt: Vector3, to_pt: Vector3, cell: Vector2i, hit: bool):
853
- start_pos = from_pt
854
- end_pos = to_pt
855
- target_cell = cell
856
- is_hit_result = hit
857
- global_position = start_pos
858
- set_process(true)
859
-
860
- func _process(delta: float):
861
- elapsed += delta
862
- var t = clampf(elapsed / duration, 0.0, 1.0)
863
-
864
- var current_ground = start_pos.lerp(end_pos, t)
865
- var parabola = 4.0 * arc_height * t * (1.0 - t)
866
- var new_pos = Vector3(current_ground.x, current_ground.y + parabola, current_ground.z)
867
-
868
- var dir = (new_pos - global_position).normalized()
869
- if dir.length_squared() > 0.001 and abs(dir.dot(Vector3.UP)) < 0.99:
870
- look_at(global_position + dir, Vector3.UP)
871
-
872
- global_position = new_pos
873
-
874
- if t >= 1.0:
875
- impacted.emit(target_cell, is_hit_result)
876
- queue_free()
877
- `,
878
- );
879
-
880
- writeFile(
881
- "scripts/audio_synth.gd",
882
- `class_name AudioSynth
883
- extends Node
884
-
885
- static func play_sound(parent: Node, sound_type: String):
886
- var player = AudioStreamPlayer.new()
887
- parent.add_child(player)
888
- player.stream = _create_stream(sound_type)
889
- player.play()
890
- player.finished.connect(func(): player.queue_free())
891
-
892
- static func _create_stream(sound_type: String) -> AudioStreamWAV:
893
- var sample_rate = 22050
894
- var duration = 0.4
895
- if sound_type == "hit" or sound_type == "sunk":
896
- duration = 0.7
897
- elif sound_type == "fire":
898
- duration = 0.45
899
- elif sound_type == "miss":
900
- duration = 0.35
901
- else:
902
- duration = 0.15
903
-
904
- var total_samples = int(sample_rate * duration)
905
- var buffer = PackedByteArray()
906
- buffer.resize(total_samples * 2)
907
-
908
- for i in range(total_samples):
909
- var t = float(i) / float(sample_rate)
910
- var progress = float(i) / float(total_samples)
911
- var sample = 0.0
912
-
913
- if sound_type == "fire":
914
- var noise_val = randf_range(-1.0, 1.0)
915
- var boom = sin(t * 90.0 * TAU) * exp(-progress * 6.0)
916
- var crack = noise_val * exp(-progress * 14.0)
917
- sample = clampf(boom * 0.7 + crack * 0.5, -1.0, 1.0)
918
-
919
- elif sound_type == "hit":
920
- var noise_val = randf_range(-1.0, 1.0)
921
- var low = sin(t * 60.0 * TAU) * exp(-progress * 4.0)
922
- var rumble = noise_val * exp(-progress * 5.0)
923
- sample = clampf(low * 0.5 + rumble * 0.8, -1.0, 1.0)
924
-
925
- elif sound_type == "sunk":
926
- var noise_val = randf_range(-1.0, 1.0)
927
- var low = sin(t * 40.0 * TAU) * exp(-progress * 2.5)
928
- var roar = noise_val * exp(-progress * 3.5)
929
- sample = clampf(low * 0.6 + roar * 0.7, -1.0, 1.0)
930
-
931
- elif sound_type == "miss":
932
- var noise_val = randf_range(-1.0, 1.0)
933
- var splash = noise_val * sin(progress * PI) * exp(-progress * 7.0)
934
- sample = clampf(splash * 0.6, -1.0, 1.0)
935
-
936
- elif sound_type == "select":
937
- var freq = 1200.0 - progress * 400.0
938
- sample = sin(t * freq * TAU) * exp(-progress * 15.0) * 0.35
939
-
940
- elif sound_type == "win":
941
- var f = 440.0 + sin(progress * 12.0) * 80.0
942
- sample = sin(t * f * TAU) * (1.0 - progress) * 0.4
943
-
944
- var int_sample = int(clampf(sample, -1.0, 1.0) * 32767.0)
945
- buffer.encode_s16(i * 2, int_sample)
946
-
947
- var stream = AudioStreamWAV.new()
948
- stream.format = AudioStreamWAV.FORMAT_16_BITS
949
- stream.mix_rate = sample_rate
950
- stream.stereo = false
951
- stream.data = buffer
952
- return stream
953
- `,
954
- );
955
-
956
- writeFile(
957
- "scripts/battle_manager.gd",
958
- `extends Node3D
959
-
960
- enum GamePhase { PLACEMENT, PLAYER_TURN, AI_TURN, GAME_OVER }
961
-
962
- @export var player_board_root: Node3D
963
- @export var enemy_board_root: Node3D
964
- @export var camera: Camera3D
965
- @export var status_label: Label
966
- @export var info_banner: Label
967
- @export var turn_indicator: Label
968
- @export var player_sunk_count_lbl: Label
969
- @export var enemy_sunk_count_lbl: Label
970
- @export var btn_randomize: Button
971
- @export var btn_start: Button
972
- @export var btn_restart: Button
973
- @export var reticle_node: Node3D
974
-
975
- var player_board: GridManager.BoardState
976
- var enemy_board: GridManager.BoardState
977
-
978
- var current_phase: GamePhase = GamePhase.PLACEMENT
979
- var hovered_grid_pos: Vector2i = Vector2i(4, 4)
980
-
981
- # AI Hunt & Target state
982
- var ai_target_candidates: Array[Vector2i] = []
983
- var rng = RandomNumberGenerator.new()
984
-
985
- var hit_marker_scene: PackedScene = preload("res://assets/models/marker_hit.scad")
986
- var miss_marker_scene: PackedScene = preload("res://assets/models/marker_miss.scad")
987
- var shell_scene: PackedScene = preload("res://assets/models/artillery_shell.scad")
988
-
989
- func _ready():
990
- rng.randomize()
991
- player_board = GridManager.BoardState.new()
992
- enemy_board = GridManager.BoardState.new()
993
-
994
- if btn_randomize:
995
- btn_randomize.pressed.connect(_on_randomize_player_fleet)
996
- if btn_start:
997
- btn_start.pressed.connect(_on_start_battle)
998
- if btn_restart:
999
- btn_restart.pressed.connect(_restart_game)
1000
-
1001
- _on_randomize_player_fleet()
1002
- GridManager.generate_random_fleet(enemy_board)
1003
-
1004
- _update_ui()
1005
- _set_phase(GamePhase.PLACEMENT)
1006
- _update_reticle_position()
1007
-
1008
- func _on_randomize_player_fleet():
1009
- if current_phase != GamePhase.PLACEMENT:
1010
- return
1011
- AudioSynth.play_sound(self, "select")
1012
- _clear_rendered_ships(player_board_root)
1013
- GridManager.generate_random_fleet(player_board)
1014
- _render_player_ships()
1015
- if info_banner:
1016
- info_banner.text = "Fleet deployed! Press 'ENGAGE!' to commence the battle."
1017
-
1018
- func _on_start_battle():
1019
- if current_phase != GamePhase.PLACEMENT:
1020
- return
1021
- AudioSynth.play_sound(self, "fire")
1022
- if btn_randomize:
1023
- btn_randomize.visible = false
1024
- if btn_start:
1025
- btn_start.visible = false
1026
- if reticle_node:
1027
- reticle_node.visible = true
1028
- _set_phase(GamePhase.PLAYER_TURN)
1029
- _update_reticle_position()
1030
- if info_banner:
1031
- info_banner.text = "Your turn! Target: [" + GridManager.get_cell_name(hovered_grid_pos) + "]. Click Left Mouse or press SPACE to fire."
1032
-
1033
- func _restart_game():
1034
- get_tree().reload_current_scene()
1035
-
1036
- func _set_phase(phase: GamePhase):
1037
- current_phase = phase
1038
- match current_phase:
1039
- GamePhase.PLACEMENT:
1040
- if turn_indicator:
1041
- turn_indicator.text = "PHASE: DEPLOYMENT"
1042
- turn_indicator.modulate = Color(0.3, 0.8, 1.0)
1043
- GamePhase.PLAYER_TURN:
1044
- if turn_indicator:
1045
- turn_indicator.text = "YOUR TURN - SELECT TARGET"
1046
- turn_indicator.modulate = Color(0.2, 1.0, 0.4)
1047
- if reticle_node:
1048
- reticle_node.visible = true
1049
- GamePhase.AI_TURN:
1050
- if turn_indicator:
1051
- turn_indicator.text = "ENEMY TURN - INCOMING SALVO..."
1052
- turn_indicator.modulate = Color(1.0, 0.35, 0.2)
1053
- if reticle_node:
1054
- reticle_node.visible = false
1055
- GamePhase.GAME_OVER:
1056
- if turn_indicator:
1057
- turn_indicator.text = "BATTLE FINISHED"
1058
- if reticle_node:
1059
- reticle_node.visible = false
1060
- if btn_restart:
1061
- btn_restart.visible = true
1062
-
1063
- func _unhandled_input(event: InputEvent):
1064
- if current_phase == GamePhase.PLAYER_TURN:
1065
- if event is InputEventMouseMotion:
1066
- _update_raycast_cursor(event.position)
1067
- elif event is InputEventMouseButton:
1068
- if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
1069
- _handle_player_fire()
1070
- elif event.is_action_pressed("ui_accept"):
1071
- _handle_player_fire()
1072
- elif event.is_action_pressed("ui_left"):
1073
- _move_reticle_grid(-1, 0)
1074
- elif event.is_action_pressed("ui_right"):
1075
- _move_reticle_grid(1, 0)
1076
- elif event.is_action_pressed("ui_up"):
1077
- _move_reticle_grid(0, -1)
1078
- elif event.is_action_pressed("ui_down"):
1079
- _move_reticle_grid(0, 1)
1080
-
1081
- func _move_reticle_grid(dx: int, dy: int):
1082
- var nx = clampi(hovered_grid_pos.x + dx, 0, GridManager.GRID_SIZE - 1)
1083
- var ny = clampi(hovered_grid_pos.y + dy, 0, GridManager.GRID_SIZE - 1)
1084
- if nx != hovered_grid_pos.x or ny != hovered_grid_pos.y:
1085
- hovered_grid_pos = Vector2i(nx, ny)
1086
- AudioSynth.play_sound(self, "select")
1087
- _update_reticle_position()
1088
-
1089
- func _update_reticle_position():
1090
- if reticle_node and enemy_board_root:
1091
- var target_local = GridManager.grid_to_local_pos(hovered_grid_pos.x, hovered_grid_pos.y)
1092
- reticle_node.global_position = enemy_board_root.to_global(target_local)
1093
- if current_phase == GamePhase.PLAYER_TURN and info_banner:
1094
- info_banner.text = "Target: Grid [" + GridManager.get_cell_name(hovered_grid_pos) + "]. Fire: Left Click or SPACE."
1095
-
1096
- func _process(_delta: float):
1097
- if current_phase == GamePhase.PLAYER_TURN and reticle_node and reticle_node.visible:
1098
- var pulse = 1.0 + 0.08 * sin(Time.get_ticks_msec() * 0.008)
1099
- reticle_node.scale = Vector3(pulse, 1.0, pulse)
1100
-
1101
- # Gentle flame flickering
1102
- var time = Time.get_ticks_msec() * 0.001
1103
- for fire in get_tree().get_nodes_in_group("fire_marker"):
1104
- var flicker = sin(time * 14.0 + fire.global_position.x * 3.0) * 0.05
1105
- fire.scale = Vector3(1.0 + flicker, 1.0 + flicker * 1.2, 1.0 + flicker)
1106
-
1107
- func _update_raycast_cursor(screen_pos: Vector2):
1108
- if not camera or not enemy_board_root:
1109
- return
1110
-
1111
- var ray_origin = camera.project_ray_origin(screen_pos)
1112
- var ray_dir = camera.project_ray_normal(screen_pos)
1113
-
1114
- if abs(ray_dir.y) < 0.001:
1115
- return
1116
- var t = -ray_origin.y / ray_dir.y
1117
- if t < 0:
1118
- return
1119
- var hit_world = ray_origin + ray_dir * t
1120
-
1121
- var enemy_local = enemy_board_root.to_local(hit_world)
1122
- if abs(enemy_local.x) <= 10.0 and abs(enemy_local.z) <= 10.0:
1123
- var grid_x = int(floor((enemy_local.x + 10.0) / GridManager.CELL_WORLD_SIZE))
1124
- var grid_y = int(floor((enemy_local.z + 10.0) / GridManager.CELL_WORLD_SIZE))
1125
- grid_x = clampi(grid_x, 0, GridManager.GRID_SIZE - 1)
1126
- grid_y = clampi(grid_y, 0, GridManager.GRID_SIZE - 1)
1127
-
1128
- if grid_x != hovered_grid_pos.x or grid_y != hovered_grid_pos.y:
1129
- hovered_grid_pos = Vector2i(grid_x, grid_y)
1130
- AudioSynth.play_sound(self, "select")
1131
- _update_reticle_position()
1132
-
1133
- func _handle_player_fire():
1134
- if enemy_board.has_shot(hovered_grid_pos.x, hovered_grid_pos.y):
1135
- if info_banner:
1136
- info_banner.text = "Grid [" + GridManager.get_cell_name(hovered_grid_pos) + "] has already been targeted! Choose another sector."
1137
- return
1138
-
1139
- _set_phase(GamePhase.AI_TURN)
1140
- if turn_indicator:
1141
- turn_indicator.text = "FIRING SALVO..."
1142
-
1143
- var is_hit = enemy_board.cell_to_ship.has(hovered_grid_pos)
1144
- var from_world = player_board_root.global_position + Vector3(0, 4.0, -2.0)
1145
- var to_world = enemy_board_root.to_global(GridManager.grid_to_local_pos(hovered_grid_pos.x, hovered_grid_pos.y))
1146
-
1147
- _spawn_shell_trajectory(from_world, to_world, hovered_grid_pos, is_hit, true)
1148
-
1149
- func _on_player_shot_impact(target_cell: Vector2i, is_hit: bool):
1150
- enemy_board.shots[target_cell] = 2 if is_hit else 1
1151
- var marker_pos = GridManager.grid_to_local_pos(target_cell.x, target_cell.y)
1152
-
1153
- if is_hit:
1154
- var ship: GridManager.ShipData = enemy_board.cell_to_ship[target_cell]
1155
- ship.hits += 1
1156
- _spawn_marker(enemy_board_root, marker_pos, true)
1157
-
1158
- if ship.is_sunk():
1159
- AudioSynth.play_sound(self, "sunk")
1160
- if info_banner:
1161
- info_banner.text = "SUNK! Enemy " + ship.name + " has been destroyed!"
1162
- _render_sunk_enemy_ship(ship)
1163
- for sc in ship.get_surrounding_cells():
1164
- if not enemy_board.has_shot(sc.x, sc.y):
1165
- enemy_board.shots[sc] = 1
1166
- var s_pos = GridManager.grid_to_local_pos(sc.x, sc.y)
1167
- _spawn_marker(enemy_board_root, s_pos, false)
1168
- else:
1169
- AudioSynth.play_sound(self, "hit")
1170
- if info_banner:
1171
- info_banner.text = "DIRECT HIT on " + ship.name + "! Bonus turn granted!"
1172
-
1173
- _update_ui()
1174
-
1175
- if enemy_board.get_all_sunk():
1176
- _trigger_victory()
1177
- return
1178
-
1179
- _set_phase(GamePhase.PLAYER_TURN)
1180
- if turn_indicator:
1181
- turn_indicator.text = "DIRECT HIT! BONUS SALVO!"
1182
- else:
1183
- AudioSynth.play_sound(self, "miss")
1184
- _spawn_marker(enemy_board_root, marker_pos, false)
1185
- if info_banner:
1186
- info_banner.text = "SPLASH! Shell missed. Enemy counter-battery firing."
1187
- _update_ui()
1188
- get_tree().create_timer(1.1).timeout.connect(_execute_ai_turn)
1189
-
1190
- func _execute_ai_turn():
1191
- if current_phase == GamePhase.GAME_OVER:
1192
- return
1193
- _set_phase(GamePhase.AI_TURN)
1194
-
1195
- var target_cell = _calculate_ai_target()
1196
- var is_hit = player_board.cell_to_ship.has(target_cell)
1197
-
1198
- var from_world = enemy_board_root.global_position + Vector3(0, 4.0, 2.0)
1199
- var to_world = player_board_root.to_global(GridManager.grid_to_local_pos(target_cell.x, target_cell.y))
1200
-
1201
- _spawn_shell_trajectory(from_world, to_world, target_cell, is_hit, false)
1202
-
1203
- func _calculate_ai_target() -> Vector2i:
1204
- while ai_target_candidates.size() > 0:
1205
- var cand = ai_target_candidates.pop_front()
1206
- if not player_board.has_shot(cand.x, cand.y):
1207
- return cand
1208
-
1209
- var untried_parity: Array[Vector2i] = []
1210
- var untried_all: Array[Vector2i] = []
1211
-
1212
- for x in range(GridManager.GRID_SIZE):
1213
- for y in range(GridManager.GRID_SIZE):
1214
- var c = Vector2i(x, y)
1215
- if not player_board.has_shot(x, y):
1216
- untried_all.append(c)
1217
- if (x + y) % 2 == 0:
1218
- untried_parity.append(c)
1219
-
1220
- if untried_parity.size() > 0:
1221
- return untried_parity[rng.randi() % untried_parity.size()]
1222
- elif untried_all.size() > 0:
1223
- return untried_all[rng.randi() % untried_all.size()]
1224
- return Vector2i(0, 0)
1225
-
1226
- func _on_ai_shot_impact(target_cell: Vector2i, is_hit: bool):
1227
- player_board.shots[target_cell] = 2 if is_hit else 1
1228
- var marker_pos = GridManager.grid_to_local_pos(target_cell.x, target_cell.y)
1229
-
1230
- if is_hit:
1231
- var ship: GridManager.ShipData = player_board.cell_to_ship[target_cell]
1232
- ship.hits += 1
1233
- _spawn_marker(player_board_root, marker_pos, true)
1234
-
1235
- for delta in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
1236
- var adj = target_cell + delta
1237
- if adj.x >= 0 and adj.x < GridManager.GRID_SIZE and adj.y >= 0 and adj.y < GridManager.GRID_SIZE:
1238
- if not player_board.has_shot(adj.x, adj.y) and not ai_target_candidates.has(adj):
1239
- ai_target_candidates.append(adj)
1240
-
1241
- if ship.is_sunk():
1242
- AudioSynth.play_sound(self, "sunk")
1243
- if info_banner:
1244
- info_banner.text = "CRITICAL LOSS! Our " + ship.name + " has been sunk!"
1245
- for sc in ship.get_surrounding_cells():
1246
- if not player_board.has_shot(sc.x, sc.y):
1247
- player_board.shots[sc] = 1
1248
- var s_pos = GridManager.grid_to_local_pos(sc.x, sc.y)
1249
- _spawn_marker(player_board_root, s_pos, false)
1250
- ai_target_candidates.erase(sc)
1251
- else:
1252
- AudioSynth.play_sound(self, "hit")
1253
- if info_banner:
1254
- info_banner.text = "WARNING! Enemy shell struck our " + ship.name + "!"
1255
-
1256
- _update_ui()
1257
-
1258
- if player_board.get_all_sunk():
1259
- _trigger_defeat()
1260
- return
1261
-
1262
- get_tree().create_timer(1.1).timeout.connect(_execute_ai_turn)
1263
- else:
1264
- AudioSynth.play_sound(self, "miss")
1265
- _spawn_marker(player_board_root, marker_pos, false)
1266
- if info_banner:
1267
- info_banner.text = "Enemy missed at sector [" + GridManager.get_cell_name(target_cell) + "]! Your turn."
1268
- _update_ui()
1269
- _set_phase(GamePhase.PLAYER_TURN)
1270
-
1271
- func _spawn_shell_trajectory(from_pt: Vector3, to_pt: Vector3, target_cell: Vector2i, is_hit: bool, is_player_shooting: bool):
1272
- AudioSynth.play_sound(self, "fire")
1273
- var shell_instance = shell_scene.instantiate()
1274
- var projectile = ShellProjectile.new()
1275
- projectile.add_child(shell_instance)
1276
- add_child(projectile)
1277
-
1278
- if is_player_shooting:
1279
- projectile.impacted.connect(_on_player_shot_impact)
1280
- else:
1281
- projectile.impacted.connect(_on_ai_shot_impact)
1282
-
1283
- projectile.launch(from_pt, to_pt, target_cell, is_hit)
1284
-
1285
- func _spawn_marker(board_root: Node3D, local_pos: Vector3, is_hit: bool):
1286
- var marker = (hit_marker_scene if is_hit else miss_marker_scene).instantiate()
1287
- board_root.add_child(marker)
1288
- if is_hit:
1289
- # Elevated slightly so the buoy base sits visibly on decks and spires rise high
1290
- marker.position = Vector3(local_pos.x, 0.25, local_pos.z)
1291
- marker.add_to_group("fire_marker")
1292
- else:
1293
- marker.position = Vector3(local_pos.x, 0.05, local_pos.z)
1294
-
1295
- func _clear_rendered_ships(board_root: Node3D):
1296
- if not board_root:
1297
- return
1298
- for child in board_root.get_children():
1299
- if child.is_in_group("ship_model"):
1300
- child.queue_free()
1301
-
1302
- func _render_player_ships():
1303
- if not player_board_root:
1304
- return
1305
- for ship in player_board.ships:
1306
- var ship_scene = load(ship.scad_path) as PackedScene
1307
- if ship_scene:
1308
- var instance = ship_scene.instantiate()
1309
- instance.add_to_group("ship_model")
1310
- player_board_root.add_child(instance)
1311
-
1312
- var length_offset = (ship.size - 1) * 0.5
1313
- var center_x = ship.origin_x + (length_offset if ship.horizontal else 0.0)
1314
- var center_y = ship.origin_y + (0.0 if ship.horizontal else length_offset)
1315
-
1316
- instance.position = Vector3(
1317
- (center_x - 4.5) * GridManager.CELL_WORLD_SIZE,
1318
- 0.05,
1319
- (center_y - 4.5) * GridManager.CELL_WORLD_SIZE
1320
- )
1321
-
1322
- if ship.horizontal:
1323
- instance.rotation_degrees.y = 90.0
1324
- else:
1325
- instance.rotation_degrees.y = 0.0
1326
-
1327
- var anim_player: AnimationPlayer = instance.find_child("AnimationPlayer", true, false)
1328
- if anim_player:
1329
- var anim_list = anim_player.get_animation_list()
1330
- if anim_list.size() > 0:
1331
- var anim_name = anim_list[0]
1332
- var anim = anim_player.get_animation(anim_name)
1333
- if anim:
1334
- anim.loop_mode = Animation.LOOP_LINEAR
1335
- anim_player.play(anim_name)
1336
-
1337
- ship.node_ref = instance
1338
-
1339
- func _render_sunk_enemy_ship(ship: GridManager.ShipData):
1340
- if not enemy_board_root:
1341
- return
1342
- var ship_scene = load(ship.scad_path) as PackedScene
1343
- if ship_scene:
1344
- var instance = ship_scene.instantiate()
1345
- instance.add_to_group("ship_model")
1346
- enemy_board_root.add_child(instance)
1347
-
1348
- var length_offset = (ship.size - 1) * 0.5
1349
- var center_x = ship.origin_x + (length_offset if ship.horizontal else 0.0)
1350
- var center_y = ship.origin_y + (0.0 if ship.horizontal else length_offset)
1351
-
1352
- instance.position = Vector3(
1353
- (center_x - 4.5) * GridManager.CELL_WORLD_SIZE,
1354
- 0.02,
1355
- (center_y - 4.5) * GridManager.CELL_WORLD_SIZE
1356
- )
1357
-
1358
- if ship.horizontal:
1359
- instance.rotation_degrees.y = 90.0
1360
- else:
1361
- instance.rotation_degrees.y = 0.0
1362
-
1363
- instance.scale = Vector3(0.95, 0.6, 0.95)
1364
-
1365
- func _update_ui():
1366
- var p_sunk = 0
1367
- for s in player_board.ships:
1368
- if s.is_sunk():
1369
- p_sunk += 1
1370
-
1371
- var e_sunk = 0
1372
- for s in enemy_board.ships:
1373
- if s.is_sunk():
1374
- e_sunk += 1
1375
-
1376
- if player_sunk_count_lbl:
1377
- player_sunk_count_lbl.text = "Losses: %d / %d" % [p_sunk, player_board.ships.size()]
1378
- if enemy_sunk_count_lbl:
1379
- enemy_sunk_count_lbl.text = "Sunk: %d / %d" % [e_sunk, enemy_board.ships.size()]
1380
-
1381
- func _trigger_victory():
1382
- _set_phase(GamePhase.GAME_OVER)
1383
- AudioSynth.play_sound(self, "win")
1384
- if info_banner:
1385
- info_banner.text = "VICTORY! The entire enemy armada has been sunk! Complete naval supremacy achieved!"
1386
- if turn_indicator:
1387
- turn_indicator.text = "VICTORY AT SEA!"
1388
- turn_indicator.modulate = Color(1.0, 0.9, 0.1)
1389
-
1390
- func _trigger_defeat():
1391
- _set_phase(GamePhase.GAME_OVER)
1392
- AudioSynth.play_sound(self, "sunk")
1393
- if info_banner:
1394
- info_banner.text = "DEFEAT! All our warships have been destroyed. Fleet ordered to retreat."
1395
- if turn_indicator:
1396
- turn_indicator.text = "DEFEAT"
1397
- turn_indicator.modulate = Color(1.0, 0.2, 0.2)
1398
- `,
1399
- );
1400
-
1401
- // =========================================================================
1402
- // 4. MAIN SCENE (.tscn)
1403
- // =========================================================================
1404
-
1405
- writeFile(
1406
- "scenes/main_scene.tscn",
1407
- `[gd_scene load_steps=10 format=3 uid="uid://c65j2xnv44v8b"]
1408
-
1409
- [ext_resource type="Script" path="res://scripts/battle_manager.gd" id="1_battle"]
1410
- [ext_resource type="PackedScene" path="res://assets/models/ocean_grid_table.scad" id="2_ocean"]
1411
- [ext_resource type="PackedScene" path="res://assets/models/targeting_reticle.scad" id="3_reticle"]
1412
-
1413
- [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
1414
- sky_top_color = Color(0.12, 0.25, 0.45, 1)
1415
- sky_horizon_color = Color(0.4, 0.6, 0.75, 1)
1416
- ground_bottom_color = Color(0.08, 0.15, 0.25, 1)
1417
- ground_horizon_color = Color(0.3, 0.5, 0.65, 1)
1418
-
1419
- [sub_resource type="Sky" id="Sky_env"]
1420
- sky_material = SubResource("ProceduralSkyMaterial_sky")
1421
-
1422
- [sub_resource type="Environment" id="Environment_main"]
1423
- background_mode = 2
1424
- sky = SubResource("Sky_env")
1425
- ambient_light_source = 3
1426
- ambient_light_color = Color(0.65, 0.75, 0.85, 1)
1427
- ambient_light_energy = 0.9
1428
- tonemap_mode = 3
1429
- glow_enabled = true
1430
- glow_intensity = 0.5
1431
- glow_bloom = 0.2
1432
-
1433
- [sub_resource type="LabelSettings" id="LabelSettings_title"]
1434
- font_size = 30
1435
- font_color = Color(0.9, 0.95, 1, 1)
1436
- outline_size = 4
1437
- outline_color = Color(0.05, 0.15, 0.25, 1)
1438
-
1439
- [sub_resource type="LabelSettings" id="LabelSettings_turn"]
1440
- font_size = 22
1441
- font_color = Color(0.2, 0.9, 1, 1)
1442
- outline_size = 3
1443
- outline_color = Color(0.02, 0.1, 0.2, 1)
1444
-
1445
- [sub_resource type="LabelSettings" id="LabelSettings_banner"]
1446
- font_size = 19
1447
- font_color = Color(1, 0.95, 0.8, 1)
1448
- outline_size = 3
1449
- outline_color = Color(0.1, 0.1, 0.15, 1)
1450
-
1451
- [node name="MainScene" type="Node3D" node_paths=PackedStringArray("player_board_root", "enemy_board_root", "camera", "status_label", "info_banner", "turn_indicator", "player_sunk_count_lbl", "enemy_sunk_count_lbl", "btn_randomize", "btn_start", "btn_restart", "reticle_node")]
1452
- script = ExtResource("1_battle")
1453
- player_board_root = NodePath("PlayerBoardRoot")
1454
- enemy_board_root = NodePath("EnemyBoardRoot")
1455
- camera = NodePath("Camera3D")
1456
- status_label = NodePath("UI/TopBar/TitleLabel")
1457
- info_banner = NodePath("UI/BottomBar/InfoBanner")
1458
- turn_indicator = NodePath("UI/TopBar/TurnIndicator")
1459
- player_sunk_count_lbl = NodePath("UI/PlayerStats/VBox/PlayerLosses")
1460
- enemy_sunk_count_lbl = NodePath("UI/EnemyStats/VBox/EnemyLosses")
1461
- btn_randomize = NodePath("UI/Controls/BtnRandomize")
1462
- btn_start = NodePath("UI/Controls/BtnStart")
1463
- btn_restart = NodePath("UI/Controls/BtnRestart")
1464
- reticle_node = NodePath("ReticleNode")
1465
-
1466
- [node name="WorldEnvironment" type="WorldEnvironment" parent="."]
1467
- environment = SubResource("Environment_main")
1468
-
1469
- [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
1470
- transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 12, 25, 15)
1471
- light_color = Color(1, 0.95, 0.9, 1)
1472
- light_energy = 1.3
1473
- shadow_enabled = true
1474
-
1475
- [node name="Camera3D" type="Camera3D" parent="."]
1476
- transform = Transform3D(1, 0, 0, 0, 0.587785, 0.809017, 0, -0.809017, 0.587785, 0, 36, 26)
1477
- fov = 48.0
1478
-
1479
- [node name="PlayerBoardRoot" type="Node3D" parent="."]
1480
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12.5, 0, 0)
1481
-
1482
- [node name="OceanTable" parent="PlayerBoardRoot" instance=ExtResource("2_ocean")]
1483
-
1484
- [node name="EnemyBoardRoot" type="Node3D" parent="."]
1485
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.5, 0, 0)
1486
-
1487
- [node name="OceanTable" parent="EnemyBoardRoot" instance=ExtResource("2_ocean")]
1488
-
1489
- [node name="ReticleNode" type="Node3D" parent="."]
1490
- visible = false
1491
-
1492
- [node name="ReticleMesh" parent="ReticleNode" instance=ExtResource("3_reticle")]
1493
-
1494
- [node name="UI" type="Control" parent="."]
1495
- layout_mode = 3
1496
- anchors_preset = 15
1497
- anchor_right = 1.0
1498
- anchor_bottom = 1.0
1499
- grow_horizontal = 2
1500
- grow_vertical = 2
1501
- mouse_filter = 2
1502
-
1503
- [node name="TopBar" type="VBoxContainer" parent="UI"]
1504
- layout_mode = 1
1505
- anchors_preset = 10
1506
- anchor_right = 1.0
1507
- offset_bottom = 85.0
1508
- grow_horizontal = 2
1509
- theme_override_constants/separation = 4
1510
-
1511
- [node name="TitleLabel" type="Label" parent="UI/TopBar"]
1512
- layout_mode = 2
1513
- text = "BATTLESHIP 3D"
1514
- label_settings = SubResource("LabelSettings_title")
1515
- horizontal_alignment = 1
1516
-
1517
- [node name="TurnIndicator" type="Label" parent="UI/TopBar"]
1518
- layout_mode = 2
1519
- text = "PHASE: DEPLOYMENT"
1520
- label_settings = SubResource("LabelSettings_turn")
1521
- horizontal_alignment = 1
1522
-
1523
- [node name="PlayerStats" type="PanelContainer" parent="UI"]
1524
- layout_mode = 1
1525
- anchors_preset = 4
1526
- anchor_top = 0.5
1527
- anchor_bottom = 0.5
1528
- offset_left = 24.0
1529
- offset_top = -60.0
1530
- offset_right = 260.0
1531
- offset_bottom = 60.0
1532
- grow_vertical = 2
1533
-
1534
- [node name="VBox" type="VBoxContainer" parent="UI/PlayerStats"]
1535
- layout_mode = 2
1536
- alignment = 1
1537
-
1538
- [node name="Label" type="Label" parent="UI/PlayerStats/VBox"]
1539
- layout_mode = 2
1540
- text = "OUR FLEET (PLAYER)"
1541
- horizontal_alignment = 1
1542
-
1543
- [node name="PlayerLosses" type="Label" parent="UI/PlayerStats/VBox"]
1544
- layout_mode = 2
1545
- text = "Losses: 0 / 10"
1546
- horizontal_alignment = 1
1547
-
1548
- [node name="EnemyStats" type="PanelContainer" parent="UI"]
1549
- layout_mode = 1
1550
- anchors_preset = 6
1551
- anchor_top = 0.5
1552
- anchor_right = 1.0
1553
- anchor_bottom = 0.5
1554
- offset_left = -260.0
1555
- offset_top = -60.0
1556
- offset_right = -24.0
1557
- offset_bottom = 60.0
1558
- grow_horizontal = 0
1559
- grow_vertical = 2
1560
-
1561
- [node name="VBox" type="VBoxContainer" parent="UI/EnemyStats"]
1562
- layout_mode = 2
1563
- alignment = 1
1564
-
1565
- [node name="Label" type="Label" parent="UI/EnemyStats/VBox"]
1566
- layout_mode = 2
1567
- text = "ENEMY FLEET"
1568
- horizontal_alignment = 1
1569
-
1570
- [node name="EnemyLosses" type="Label" parent="UI/EnemyStats/VBox"]
1571
- layout_mode = 2
1572
- text = "Sunk: 0 / 10"
1573
- horizontal_alignment = 1
1574
-
1575
- [node name="BottomBar" type="VBoxContainer" parent="UI"]
1576
- layout_mode = 1
1577
- anchors_preset = 12
1578
- anchor_top = 1.0
1579
- anchor_right = 1.0
1580
- anchor_bottom = 1.0
1581
- offset_top = -120.0
1582
- offset_bottom = -20.0
1583
- grow_horizontal = 2
1584
- grow_vertical = 0
1585
- alignment = 1
1586
-
1587
- [node name="InfoBanner" type="Label" parent="UI/BottomBar"]
1588
- layout_mode = 2
1589
- text = "Deploy your fleet and prepare for naval battle!"
1590
- label_settings = SubResource("LabelSettings_banner")
1591
- horizontal_alignment = 1
1592
-
1593
- [node name="Controls" type="HBoxContainer" parent="UI"]
1594
- layout_mode = 1
1595
- anchors_preset = 7
1596
- anchor_left = 0.5
1597
- anchor_top = 1.0
1598
- anchor_right = 0.5
1599
- anchor_bottom = 1.0
1600
- offset_left = -250.0
1601
- offset_top = -65.0
1602
- offset_right = 250.0
1603
- offset_bottom = -15.0
1604
- grow_horizontal = 2
1605
- grow_vertical = 0
1606
- theme_override_constants/separation = 20
1607
- alignment = 1
1608
-
1609
- [node name="BtnRandomize" type="Button" parent="UI/Controls"]
1610
- custom_minimum_size = Vector2(175, 45)
1611
- layout_mode = 2
1612
- text = "Randomize Fleet"
1613
-
1614
- [node name="BtnStart" type="Button" parent="UI/Controls"]
1615
- custom_minimum_size = Vector2(140, 45)
1616
- layout_mode = 2
1617
- text = "ENGAGE!"
1618
-
1619
- [node name="BtnRestart" type="Button" parent="UI/Controls"]
1620
- visible = false
1621
- custom_minimum_size = Vector2(160, 45)
1622
- layout_mode = 2
1623
- text = "Play Again"
1624
- `,
1625
- );
1626
-
1627
- // =========================================================================
1628
- // 5. PROJECT CONFIG & README
1629
- // =========================================================================
1630
-
1631
- writeFile(
1632
- "project.godot",
1633
- `config_version=5
1634
-
1635
- [application]
1636
-
1637
- config/name="Battleship 3D"
1638
- config/description="3D Naval Battleship game featuring procedural OpenSCAD warships and tactical AI"
1639
- run/main_scene="res://scenes/main_scene.tscn"
1640
- config/features=PackedStringArray("4.3", "Forward Plus")
1641
-
1642
- [display]
1643
-
1644
- window/size/viewport_width=1280
1645
- window/size/viewport_height=720
1646
- window/stretch/mode="canvas_items"
1647
- window/stretch/aspect="expand"
1648
-
1649
- [editor_plugins]
1650
-
1651
- enabled=PackedStringArray("res://addons/scad_importer/plugin.cfg")
1652
-
1653
- [rendering]
1654
-
1655
- anti_aliasing/quality/msaa_3d=2
1656
- anti_aliasing/quality/screen_space_aa=1
1657
- `,
1658
- );
1659
-
1660
- writeFile(
1661
- ".gitignore",
1662
- `.godot/
1663
- *.translation
1664
- *.tmp
1665
- scad_cache_*
1666
- `,
1667
- );
1668
-
1669
- writeFile(
1670
- "README.md",
1671
- `# Battleship 3D
1672
-
1673
- Tactical 3D Battleship game built for Godot 4 using procedural OpenSCAD 3D models.
1674
-
1675
- ## Features
1676
- - **10 Ships Classic Armada**:
1677
- - 1 × Battleship (4 cells)
1678
- - 2 × Cruisers (3 cells)
1679
- - 3 × Destroyers (2 cells)
1680
- - 4 × Patrol Boats (1 cell)
1681
- - **Standard Clearance Rules**: Ships cannot touch horizontally, vertically, or diagonally. Sunk enemy ships are automatically ringed with miss markers.
1682
- - **Smart Tactical AI**: Uses a Hunt-and-Target algorithm with checkerboard parity scanning followed by cardinal searches on hits.
1683
- - **Elevated Flame Spires**: Beautiful glowing crimson and plasma crystal flame pillars rising ~2.8 meters so they remain clearly visible above ship decks and bridge superstructures.
1684
- - **Accurate HUD Reticle**: Inward-framing bracket corners precisely defining the targeted 2x2m sector.
1685
- - **Controls**:
1686
- - **Mouse**: Aim at the enemy grid and click Left Mouse Button to fire.
1687
- - **Keyboard**: Use Arrow Keys / WASD to move the reticle across sectors A-1 through J-10, and press Space or Enter to fire.
1688
- `,
1689
- );
1690
-
1691
- console.log(`\nUpdated all files successfully in '${ROOT_DIR}/'!`);