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.
- package/README.md +1 -5
- package/godot/README.md +0 -3
- package/package.json +1 -3
- package/bevy/README.md +0 -46
- package/bevy/examples/LICENSE +0 -21
- package/bevy/examples/README.md +0 -58
- package/bevy/examples/package.json +0 -5
- package/bevy/examples/packman_3d.js +0 -2415
- 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,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}/'!`);
|