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,2753 +0,0 @@
1
- const fs = require("fs");
2
- const path = require("path");
3
-
4
- const ROOT_DIR = path.join(process.cwd(), "horde_survivor_3d");
5
-
6
- function ensureDir(dirPath) {
7
- if (!fs.existsSync(dirPath)) {
8
- fs.mkdirSync(dirPath, { recursive: true });
9
- }
10
- }
11
-
12
- function writeFile(relPath, content) {
13
- const fullPath = path.join(ROOT_DIR, relPath);
14
- ensureDir(path.dirname(fullPath));
15
- fs.writeFileSync(fullPath, content.trimStart(), "utf-8");
16
- console.log(`[Created] ${relPath}`);
17
- }
18
-
19
- console.log(
20
- `Generating Horde Survivor 3D (Refined Models & Fast Grid Physics) in: ${ROOT_DIR}`,
21
- );
22
- ensureDir(ROOT_DIR);
23
-
24
- // =========================================================================
25
- // 1. REFINED OPENSCAD 3D PROCEDURAL ASSETS
26
- // (OpenSCAD +X Right -> Godot +X, +Y Forward -> Godot -Z, +Z Up -> Godot +Y)
27
- // =========================================================================
28
-
29
- // assets/player.scad (Stylized Paladin Knight)
30
- writeFile(
31
- "assets/player.scad",
32
- `
33
- $fn = 24;
34
- $asa = 35.0;
35
-
36
- anim_data = [
37
- ["Walk", [
38
- ["Root", [
39
- [0.0, [0, 0, 0], [0, 0, 0]],
40
- [0.2, [2, 0, 2], [0, 0, 0.12]],
41
- [0.4, [0, 0, 0], [0, 0, 0]],
42
- [0.6, [-2, 0, -2], [0, 0, 0.12]],
43
- [0.8, [0, 0, 0], [0, 0, 0]]
44
- ]],
45
- ["LegL", [
46
- [0.0, [-26, 0, 0], [-0.65, 0, 1.5]],
47
- [0.2, [0, 0, 0], [-0.65, 0, 1.65]],
48
- [0.4, [26, 0, 0], [-0.65, 0, 1.5]],
49
- [0.6, [0, 0, 0], [-0.65, 0, 1.65]],
50
- [0.8, [-26, 0, 0], [-0.65, 0, 1.5]]
51
- ]],
52
- ["LegR", [
53
- [0.0, [26, 0, 0], [0.65, 0, 1.5]],
54
- [0.2, [0, 0, 0], [0.65, 0, 1.65]],
55
- [0.4, [-26, 0, 0], [0.65, 0, 1.5]],
56
- [0.6, [0, 0, 0], [0.65, 0, 1.65]],
57
- [0.8, [26, 0, 0], [0.65, 0, 1.5]]
58
- ]],
59
- ["ArmL", [
60
- [0.0, [15, 0, -5], [-1.4, 0, 2.9]],
61
- [0.4, [-15, 0, -5], [-1.4, 0, 2.9]],
62
- [0.8, [15, 0, -5], [-1.4, 0, 2.9]]
63
- ]],
64
- ["ArmR", [
65
- [0.0, [-22, 0, 5], [1.4, 0, 2.9]],
66
- [0.4, [22, 0, 5], [1.4, 0, 2.9]],
67
- [0.8, [-22, 0, 5], [1.4, 0, 2.9]]
68
- ]]
69
- ]]
70
- ];
71
-
72
- armature(animations=anim_data) {
73
- bone(name="Root", t=[0, 0, 0], r=[0, 0, 0]) {
74
- // Torso / Cuirass
75
- translate([0, 0, 2.3]) {
76
- // Main steel chestplate
77
- color([0.3, 0.34, 0.42], metalness=0.9, roughness=0.25)
78
- cube([1.7, 1.2, 1.5], center=true);
79
-
80
- // Gold pectoral trim (+Y forward)
81
- translate([0, 0.61, 0.25])
82
- color([0.92, 0.75, 0.15], metalness=0.95, roughness=0.2, emissive=[0.3, 0.2, 0.0], emissiveIntensity=1.0)
83
- cube([1.1, 0.08, 0.35], center=true);
84
-
85
- // Mana belt & buckle
86
- translate([0, 0, -0.65]) {
87
- color([0.18, 0.15, 0.12], metalness=0.2, roughness=0.8)
88
- cube([1.74, 1.24, 0.3], center=true);
89
- translate([0, 0.63, 0])
90
- color([0.0, 0.85, 1.0], metalness=0.1, roughness=0.2, emissive=[0.0, 0.85, 1.0], emissiveIntensity=3.0)
91
- cube([0.5, 0.08, 0.32], center=true);
92
- }
93
- }
94
-
95
- // Great Helmet
96
- translate([0, 0, 3.5]) {
97
- color([0.32, 0.36, 0.45], metalness=0.9, roughness=0.2)
98
- cube([1.25, 1.25, 1.25], center=true);
99
-
100
- // Helmet visor slit plate
101
- translate([0, 0.64, 0.05])
102
- color([0.2, 0.22, 0.28], metalness=0.95, roughness=0.15)
103
- cube([1.1, 0.06, 0.4], center=true);
104
-
105
- // Glowing Cyan Visor Line
106
- translate([0, 0.68, 0.05])
107
- color([0.0, 0.95, 1.0], metalness=0.0, roughness=0.1, emissive=[0.0, 0.95, 1.0], emissiveIntensity=4.0)
108
- cube([0.9, 0.04, 0.16], center=true);
109
-
110
- // Royal Plume crest flowing along -Y
111
- translate([0, -0.2, 0.85])
112
- color([0.85, 0.12, 0.18], metalness=0.1, roughness=0.7)
113
- cube([0.25, 1.3, 0.55], center=true);
114
- }
115
-
116
- // Left Leg
117
- bone(name="LegL", t=[-0.65, 0, 1.5], r=[0, 0, 0]) {
118
- color([0.25, 0.28, 0.35], metalness=0.85, roughness=0.35) {
119
- translate([0, 0, -0.75])
120
- cube([0.5, 0.65, 1.5], center=true);
121
- // Sabaton (boot tip +Y)
122
- translate([0, 0.18, -1.35])
123
- cube([0.52, 0.8, 0.3], center=true);
124
- }
125
- }
126
-
127
- // Right Leg
128
- bone(name="LegR", t=[0.65, 0, 1.5], r=[0, 0, 0]) {
129
- color([0.25, 0.28, 0.35], metalness=0.85, roughness=0.35) {
130
- translate([0, 0, -0.75])
131
- cube([0.5, 0.65, 1.5], center=true);
132
- // Sabaton
133
- translate([0, 0.18, -1.35])
134
- cube([0.52, 0.8, 0.3], center=true);
135
- }
136
- }
137
-
138
- // Left Arm (Shield Arm)
139
- bone(name="ArmL", t=[-1.4, 0, 2.9], r=[0, 0, 0]) {
140
- // Shoulder pauldron
141
- translate([-0.1, 0, 0.1])
142
- color([0.35, 0.4, 0.5], metalness=0.9, roughness=0.25)
143
- cube([0.7, 0.85, 0.5], center=true);
144
- // Arm
145
- translate([0, 0, -0.65])
146
- color([0.25, 0.28, 0.35], metalness=0.8, roughness=0.4)
147
- cube([0.45, 0.45, 1.3], center=true);
148
-
149
- // Medieval Kite Shield mounted on forearm
150
- translate([-0.35, 0.35, -0.5]) {
151
- // Dark blue steel body
152
- color([0.15, 0.25, 0.5], metalness=0.7, roughness=0.3)
153
- cube([0.16, 1.0, 1.6], center=true);
154
- // Gold rim
155
- translate([-0.09, 0, 0])
156
- color([0.92, 0.75, 0.15], metalness=0.95, roughness=0.2, emissive=[0.3, 0.2, 0.0], emissiveIntensity=1.0) {
157
- cube([0.04, 0.9, 0.18], center=true);
158
- cube([0.04, 0.2, 1.4], center=true);
159
- }
160
- }
161
- }
162
-
163
- // Right Arm (Sword Arm)
164
- bone(name="ArmR", t=[1.4, 0, 2.9], r=[0, 0, 0]) {
165
- // Shoulder pauldron
166
- translate([0.1, 0, 0.1])
167
- color([0.35, 0.4, 0.5], metalness=0.9, roughness=0.25)
168
- cube([0.7, 0.85, 0.5], center=true);
169
-
170
- // Arm
171
- translate([0, 0, -0.65])
172
- color([0.25, 0.28, 0.35], metalness=0.8, roughness=0.4)
173
- cube([0.45, 0.45, 1.3], center=true);
174
-
175
- // Broadsword held in hand, angled forward (+Y) and up (+Z)
176
- translate([0.05, 0.3, -1.05])
177
- rotate([-45, 0, 0]) {
178
- // Pommel
179
- translate([0, 0, -0.36])
180
- color([0.9, 0.75, 0.15], metalness=0.95, roughness=0.25)
181
- sphere(r=0.13);
182
-
183
- // Grip / Handle
184
- translate([0, 0, -0.16])
185
- color([0.2, 0.16, 0.12], metalness=0.1, roughness=0.8)
186
- cube([0.14, 0.14, 0.32], center=true);
187
-
188
- // Crossguard
189
- translate([0, 0, 0.04])
190
- color([0.9, 0.75, 0.15], metalness=0.95, roughness=0.25)
191
- cube([0.85, 0.2, 0.12], center=true);
192
-
193
- // Steel Blade
194
- translate([0, 0, 1.1]) {
195
- color([0.9, 0.95, 1.0], metalness=0.98, roughness=0.12)
196
- cube([0.08, 0.32, 2.0], center=true);
197
-
198
- // Glowing runic fuller
199
- color([0.0, 0.85, 1.0], metalness=0.0, roughness=0.1, emissive=[0.0, 0.85, 1.0], emissiveIntensity=2.5)
200
- cube([0.1, 0.1, 1.5], center=true);
201
- }
202
- }
203
- }
204
- }
205
- }
206
- `,
207
- );
208
-
209
- // assets/enemy_walker.scad (Terrifying Zombie with sunken eye sockets and ragged clothes)
210
- writeFile(
211
- "assets/enemy_walker.scad",
212
- `
213
- $fn = 20;
214
- $asa = 30.0;
215
-
216
- anim_data = [
217
- ["Walk", [
218
- ["Torso", [
219
- [0.0, [12, 0, 3], [0, 0, 1.8]],
220
- [0.3, [14, 0, -3], [0, 0, 1.95]],
221
- [0.6, [12, 0, 3], [0, 0, 1.8]]
222
- ]],
223
- ["LegL", [
224
- [0.0, [-28, 0, 0], [-0.55, 0, 1.4]],
225
- [0.3, [28, 0, 0], [-0.55, 0, 1.4]],
226
- [0.6, [-28, 0, 0], [-0.55, 0, 1.4]]
227
- ]],
228
- ["LegR", [
229
- [0.0, [28, 0, 0], [0.55, 0, 1.4]],
230
- [0.3, [-28, 0, 0], [0.55, 0, 1.4]],
231
- [0.6, [28, 0, 0], [0.55, 0, 1.4]]
232
- ]]
233
- ]]
234
- ];
235
-
236
- armature(animations=anim_data) {
237
- bone(name="Torso", t=[0, 0, 1.8], r=[12, 0, 0]) {
238
- // Decaying Green Flesh
239
- color([0.22, 0.38, 0.26], metalness=0.1, roughness=0.85)
240
- cube([1.3, 0.95, 1.4], center=true);
241
-
242
- // Ripped dark tunic
243
- translate([0, 0, -0.25])
244
- color([0.18, 0.16, 0.15], metalness=0.05, roughness=0.9)
245
- cube([1.35, 1.0, 0.7], center=true);
246
-
247
- // Exposed Decayed Ribs (+Y front)
248
- translate([0, 0.5, 0.05])
249
- color([0.8, 0.8, 0.75], metalness=0.1, roughness=0.6) {
250
- cube([0.8, 0.08, 0.1], center=true);
251
- translate([0, 0, -0.2])
252
- cube([0.7, 0.08, 0.1], center=true);
253
- }
254
-
255
- // Zombie Head
256
- translate([0, 0.35, 1.1]) {
257
- // Skull
258
- color([0.25, 0.42, 0.28], metalness=0.1, roughness=0.85)
259
- cube([1.05, 1.05, 1.05], center=true);
260
-
261
- // Sunken Eye Sockets (Dark recessed boxes)
262
- translate([-0.28, 0.53, 0.1])
263
- color([0.06, 0.06, 0.06], roughness=1.0)
264
- cube([0.3, 0.08, 0.3], center=true);
265
- translate([0.28, 0.53, 0.1])
266
- color([0.06, 0.06, 0.06], roughness=1.0)
267
- cube([0.3, 0.08, 0.3], center=true);
268
-
269
- // Glowing Necrotic Crimson Pupils inside sockets
270
- translate([-0.28, 0.56, 0.1])
271
- color([1.0, 0.1, 0.1], metalness=0.0, roughness=0.1, emissive=[1.0, 0.05, 0.05], emissiveIntensity=4.0)
272
- sphere(r=0.1);
273
- translate([0.28, 0.56, 0.1])
274
- color([1.0, 0.1, 0.1], metalness=0.0, roughness=0.1, emissive=[1.0, 0.05, 0.05], emissiveIntensity=4.0)
275
- sphere(r=0.1);
276
-
277
- // Slack Decaying Jaw & Teeth
278
- translate([0, 0.42, -0.38]) {
279
- color([0.2, 0.34, 0.22], roughness=0.9)
280
- cube([0.85, 0.7, 0.35], center=true);
281
- // Jagged teeth
282
- translate([0, 0.35, 0.1])
283
- color([0.85, 0.85, 0.8], roughness=0.5)
284
- cube([0.65, 0.06, 0.12], center=true);
285
- }
286
- }
287
-
288
- // Outstretched Grasping Arms (+Y forward)
289
- translate([-0.95, 0.75, 0.3]) {
290
- color([0.22, 0.38, 0.26], roughness=0.85)
291
- cube([0.35, 1.4, 0.35], center=true);
292
- // Claws
293
- translate([0, 0.75, -0.05])
294
- color([0.15, 0.15, 0.15], roughness=0.6)
295
- cube([0.32, 0.2, 0.15], center=true);
296
- }
297
- translate([0.95, 0.75, 0.3]) {
298
- color([0.22, 0.38, 0.26], roughness=0.85)
299
- cube([0.35, 1.4, 0.35], center=true);
300
- translate([0, 0.75, -0.05])
301
- color([0.15, 0.15, 0.15], roughness=0.6)
302
- cube([0.32, 0.2, 0.15], center=true);
303
- }
304
- }
305
-
306
- // Tattered Legs (Feet at Z = 0)
307
- bone(name="LegL", t=[-0.55, 0, 1.4], r=[0, 0, 0]) {
308
- color([0.18, 0.16, 0.15], roughness=0.9)
309
- translate([0, 0, -0.7])
310
- cube([0.48, 0.55, 1.4], center=true);
311
- }
312
-
313
- bone(name="LegR", t=[0.55, 0, 1.4], r=[0, 0, 0]) {
314
- color([0.18, 0.16, 0.15], roughness=0.9)
315
- translate([0, 0, -0.7])
316
- cube([0.48, 0.55, 1.4], center=true);
317
- }
318
- }
319
- `,
320
- );
321
-
322
- // assets/enemy_swarmer.scad (Vampiric Bat with swept organic wings)
323
- writeFile(
324
- "assets/enemy_swarmer.scad",
325
- `
326
- $fn = 18;
327
- $asa = 30.0;
328
-
329
- anim_data = [
330
- ["Fly", [
331
- ["Body", [
332
- [0.0, [0, 0, 0], [0, 0, 1.3]],
333
- [0.2, [4, 0, 0], [0, 0, 1.5]],
334
- [0.4, [0, 0, 0], [0, 0, 1.3]]
335
- ]],
336
- ["WingL", [
337
- [0.0, [0, -35, 0], [-0.5, 0, 1.3]],
338
- [0.2, [0, 32, 0], [-0.5, 0, 1.3]],
339
- [0.4, [0, -35, 0], [-0.5, 0, 1.3]]
340
- ]],
341
- ["WingR", [
342
- [0.0, [0, 35, 0], [0.5, 0, 1.3]],
343
- [0.2, [0, -32, 0], [0.5, 0, 1.3]],
344
- [0.4, [0, 35, 0], [0.5, 0, 1.3]]
345
- ]]
346
- ]]
347
- ];
348
-
349
- module bat_wing() {
350
- rotate([6, -8, -4]) {
351
- // Swept Scalloped Leather Membrane
352
- color([0.48, 0.15, 0.34], metalness=0.1, roughness=0.7)
353
- linear_extrude(height=0.028, center=true)
354
- polygon([
355
- [0.0, 0.0],
356
- [0.85, 0.08],
357
- [1.65, 0.04],
358
- [2.45, -0.12],
359
- // Scallop 1 (outer tip to middle finger)
360
- [2.30, -0.30],
361
- [2.12, -0.42],
362
- [1.98, -0.52],
363
- [1.88, -0.70],
364
- // Scallop 2 (middle finger to inner finger)
365
- [1.70, -0.58],
366
- [1.52, -0.50],
367
- [1.34, -0.56],
368
- [1.20, -0.72],
369
- // Scallop 3 (inner finger to body root)
370
- [0.95, -0.58],
371
- [0.65, -0.48],
372
- [0.35, -0.40],
373
- [0.05, -0.30]
374
- ]);
375
-
376
- // Skeletal Wing Bones (Upper arm, forearm, and radiating finger ribs)
377
- color([0.32, 0.14, 0.28], metalness=0.3, roughness=0.5) {
378
- // Upper arm
379
- hull() {
380
- sphere(r=0.065);
381
- translate([0.85, 0.08, 0]) sphere(r=0.055);
382
- }
383
- // Forearm
384
- hull() {
385
- translate([0.85, 0.08, 0]) sphere(r=0.055);
386
- translate([1.65, 0.04, 0]) sphere(r=0.048);
387
- }
388
- // Finger 1 (Outer wing strut to tip)
389
- hull() {
390
- translate([1.65, 0.04, 0]) sphere(r=0.045);
391
- translate([2.45, -0.12, 0]) sphere(r=0.015);
392
- }
393
- // Finger 2 (Middle rib)
394
- hull() {
395
- translate([1.65, 0.04, 0]) sphere(r=0.04);
396
- translate([1.88, -0.70, 0]) sphere(r=0.015);
397
- }
398
- // Finger 3 (Inner rib)
399
- hull() {
400
- translate([1.65, 0.04, 0]) sphere(r=0.035);
401
- translate([1.20, -0.72, 0]) sphere(r=0.015);
402
- }
403
- // Skeletal Knuckles
404
- translate([0.85, 0.08, 0]) sphere(r=0.065);
405
- translate([1.65, 0.04, 0]) sphere(r=0.058);
406
- }
407
-
408
- // Elbow Thumb Claw
409
- translate([0.85, 0.11, 0.03])
410
- rotate([-30, 15, 15])
411
- color([0.18, 0.10, 0.18], metalness=0.4, roughness=0.3)
412
- cylinder(h=0.16, r1=0.035, r2=0.005, center=true);
413
- }
414
- }
415
-
416
- armature(animations=anim_data) {
417
- bone(name="Body", t=[0, 0, 1.3], r=[0, 0, 0]) {
418
- // Sleek Vampiric Bat Head & Body
419
- color([0.22, 0.12, 0.28], metalness=0.2, roughness=0.6)
420
- sphere(r=0.65);
421
-
422
- // Pointed Bat Ears
423
- translate([-0.3, 0.05, 0.65])
424
- rotate([10, -15, 0])
425
- color([0.35, 0.15, 0.38], metalness=0.1, roughness=0.7)
426
- cylinder(h=0.7, r1=0.22, r2=0.03, center=true);
427
- translate([0.3, 0.05, 0.65])
428
- rotate([10, 15, 0])
429
- color([0.35, 0.15, 0.38], metalness=0.1, roughness=0.7)
430
- cylinder(h=0.7, r1=0.22, r2=0.03, center=true);
431
-
432
- // Glowing Golden Eyes (+Y forward)
433
- translate([-0.24, 0.52, 0.15])
434
- color([1.0, 0.85, 0.0], metalness=0.0, roughness=0.1, emissive=[1.0, 0.85, 0.0], emissiveIntensity=3.5)
435
- sphere(r=0.14);
436
- translate([0.24, 0.52, 0.15])
437
- color([1.0, 0.85, 0.0], metalness=0.0, roughness=0.1, emissive=[1.0, 0.85, 0.0], emissiveIntensity=3.5)
438
- sphere(r=0.14);
439
-
440
- // Vampire Fangs
441
- translate([-0.15, 0.58, -0.20])
442
- rotate([15, 0, 0])
443
- color([0.95, 0.95, 0.95], roughness=0.3)
444
- cylinder(h=0.32, r1=0.01, r2=0.06, center=true);
445
- translate([0.15, 0.58, -0.20])
446
- rotate([15, 0, 0])
447
- color([0.95, 0.95, 0.95], roughness=0.3)
448
- cylinder(h=0.32, r1=0.01, r2=0.06, center=true);
449
- }
450
-
451
- // Left Wing
452
- bone(name="WingL", t=[-0.5, 0, 1.3], r=[0, 0, 0]) {
453
- mirror([1, 0, 0])
454
- bat_wing();
455
- }
456
-
457
- // Right Wing
458
- bone(name="WingR", t=[0.5, 0, 1.3], r=[0, 0, 0]) {
459
- bat_wing();
460
- }
461
- }
462
- `,
463
- );
464
-
465
- // assets/enemy_brute.scad (Heavy Armored Boss with FORWARD spikes and Spiked War Maul)
466
- writeFile(
467
- "assets/enemy_brute.scad",
468
- `
469
- $fn = 24;
470
- $asa = 35.0;
471
-
472
- anim_data = [
473
- ["Walk", [
474
- ["Torso", [
475
- [0.0, [0, 3, 0], [0, 0, 2.2]],
476
- [0.4, [0, -3, 0], [0, 0, 2.4]],
477
- [0.8, [0, 3, 0], [0, 0, 2.2]]
478
- ]],
479
- ["ClubArm", [
480
- [0.0, [-20, 0, 0], [2.1, 0, 2.8]],
481
- [0.4, [25, 0, 0], [2.1, 0, 2.8]],
482
- [0.8, [-20, 0, 0], [2.1, 0, 2.8]]
483
- ]]
484
- ]]
485
- ];
486
-
487
- armature(animations=anim_data) {
488
- bone(name="Torso", t=[0, 0, 2.2], r=[0, 0, 0]) {
489
- // Heavy Muscular Body
490
- color([0.35, 0.2, 0.16], metalness=0.1, roughness=0.8)
491
- cube([2.5, 1.9, 2.2], center=true);
492
-
493
- // Heavy Iron Breastplate
494
- translate([0, 0.85, 0.1]) {
495
- color([0.18, 0.2, 0.24], metalness=0.9, roughness=0.25)
496
- cube([2.2, 0.35, 1.8], center=true);
497
-
498
- // CRITICAL FIX: Spikes point FORWARD (+Y) out of the chest armor, NOT UP!
499
- translate([-0.6, 0.2, 0.3])
500
- rotate([-90, 0, 0])
501
- color([1.0, 0.45, 0.1], metalness=0.8, roughness=0.2, emissive=[0.8, 0.25, 0.0], emissiveIntensity=2.0)
502
- cylinder(h=0.85, r1=0.28, r2=0.0);
503
-
504
- translate([0.6, 0.2, 0.3])
505
- rotate([-90, 0, 0])
506
- color([1.0, 0.45, 0.1], metalness=0.8, roughness=0.2, emissive=[0.8, 0.25, 0.0], emissiveIntensity=2.0)
507
- cylinder(h=0.85, r1=0.28, r2=0.0);
508
- }
509
-
510
- // Heavy Spiked Pauldron on Left Shoulder
511
- translate([-1.6, 0, 0.9]) {
512
- color([0.2, 0.22, 0.28], metalness=0.9, roughness=0.25)
513
- cube([0.9, 1.1, 0.7], center=true);
514
- // Pauldron Spike pointing up/out
515
- translate([-0.2, 0, 0.4])
516
- rotate([0, -25, 0])
517
- color([0.8, 0.8, 0.8], metalness=0.9, roughness=0.2)
518
- cylinder(h=0.7, r1=0.2, r2=0.0);
519
- }
520
-
521
- // Demonic Horned Head
522
- translate([0, 0.35, 1.7]) {
523
- color([0.28, 0.15, 0.12], metalness=0.1, roughness=0.85)
524
- cube([1.35, 1.35, 1.35], center=true);
525
-
526
- // Curved Demonic Horns
527
- translate([-0.8, -0.05, 0.5])
528
- rotate([15, -30, 0])
529
- color([0.12, 0.12, 0.12], metalness=0.6, roughness=0.4)
530
- cylinder(h=1.3, r1=0.3, r2=0.04);
531
-
532
- translate([0.8, -0.05, 0.5])
533
- rotate([15, 30, 0])
534
- color([0.12, 0.12, 0.12], metalness=0.6, roughness=0.4)
535
- cylinder(h=1.3, r1=0.3, r2=0.04);
536
-
537
- // Crimson Slit Visor (+Y forward)
538
- translate([0, 0.7, 0.1])
539
- color([1.0, 0.1, 0.0], metalness=0.0, roughness=0.1, emissive=[1.0, 0.1, 0.0], emissiveIntensity=4.5)
540
- cube([0.9, 0.08, 0.25], center=true);
541
- }
542
-
543
- // Heavy Plated Legs (Bottom of boots at Z = 0)
544
- translate([-0.75, 0, -1.6])
545
- color([0.2, 0.22, 0.26], metalness=0.8, roughness=0.35)
546
- cube([0.85, 1.1, 1.2], center=true);
547
- translate([0.75, 0, -1.6])
548
- color([0.2, 0.22, 0.26], metalness=0.8, roughness=0.35)
549
- cube([0.85, 1.1, 1.2], center=true);
550
-
551
- // Left Arm
552
- translate([-1.6, 0.2, 0.1])
553
- color([0.35, 0.2, 0.16], roughness=0.8)
554
- cube([0.75, 0.75, 1.6], center=true);
555
- }
556
-
557
- // Right Arm wielding giant Spiked War Maul
558
- bone(name="ClubArm", t=[2.1, 0, 2.8], r=[0, 0, 0]) {
559
- color([0.35, 0.2, 0.16], roughness=0.8)
560
- cube([0.75, 0.75, 1.5], center=true);
561
-
562
- // Maul Handle
563
- translate([0, 0.5, -0.4]) {
564
- color([0.25, 0.18, 0.12], metalness=0.2, roughness=0.8)
565
- cylinder(h=3.2, r=0.16, center=true);
566
-
567
- // Giant Steel Maul Head
568
- translate([0, 0, 1.8]) {
569
- color([0.18, 0.2, 0.25], metalness=0.95, roughness=0.2)
570
- cube([1.1, 1.1, 1.3], center=true);
571
- // Glowing Molten Lava Core
572
- color([1.0, 0.35, 0.0], metalness=0.0, roughness=0.1, emissive=[1.0, 0.35, 0.0], emissiveIntensity=4.0)
573
- sphere(r=0.45);
574
- // Striking Spikes
575
- translate([0, 0.65, 0])
576
- rotate([-90, 0, 0])
577
- color([0.9, 0.9, 0.9], metalness=0.9, roughness=0.2)
578
- cylinder(h=0.5, r1=0.2, r2=0.0);
579
- translate([0, -0.65, 0])
580
- rotate([90, 0, 0])
581
- color([0.9, 0.9, 0.9], metalness=0.9, roughness=0.2)
582
- cylinder(h=0.5, r1=0.2, r2=0.0);
583
- }
584
- }
585
- }
586
- }
587
- `,
588
- );
589
-
590
- // assets/gem_xp.scad (Brilliant Faceted Octahedron Crystal)
591
- writeFile(
592
- "assets/gem_xp.scad",
593
- `
594
- $fn = 8;
595
- $asa = 0.0;
596
-
597
- anim_data = [
598
- ["Spin", [
599
- ["Gem", [
600
- [0.0, [0, 0, 0], [0, 0, 0.6]],
601
- [0.5, [0, 0, 90], [0, 0, 0.85]],
602
- [1.0, [0, 0, 180], [0, 0, 0.6]],
603
- [1.5, [0, 0, 270], [0, 0, 0.35]],
604
- [2.0, [0, 0, 360], [0, 0, 0.6]]
605
- ]]
606
- ]]
607
- ];
608
-
609
- armature(animations=anim_data) {
610
- bone(name="Gem", t=[0, 0, 0.6], r=[0, 0, 0]) {
611
- color([0.1, 0.85, 1.0], alpha=0.9, metalness=0.2, roughness=0.1, emissive=[0.05, 0.7, 0.95], emissiveIntensity=3.5, specularIntensity=2.0)
612
- cylinder(h=0.7, r1=0.6, r2=0.0);
613
-
614
- translate([0, 0, -0.7])
615
- color([0.1, 0.85, 1.0], alpha=0.9, metalness=0.2, roughness=0.1, emissive=[0.05, 0.7, 0.95], emissiveIntensity=3.5, specularIntensity=2.0)
616
- cylinder(h=0.7, r1=0.0, r2=0.6);
617
-
618
- color([1.0, 1.0, 1.0], metalness=0.0, roughness=0.0, emissive=[1.0, 1.0, 1.0], emissiveIntensity=4.5)
619
- sphere(r=0.25, $fn=12);
620
- }
621
- }
622
- `,
623
- );
624
-
625
- // assets/orb.scad (Holy Aegis Orbiting Shield Orb)
626
- writeFile(
627
- "assets/orb.scad",
628
- `
629
- $fn = 20;
630
- $asa = 45.0;
631
-
632
- anim_data = [
633
- ["Pulse", [
634
- ["Rings", [
635
- [0.0, [0, 0, 0]],
636
- [0.5, [45, 90, 0]],
637
- [1.0, [90, 180, 0]],
638
- [1.5, [45, 270, 0]],
639
- [2.0, [0, 360, 0]]
640
- ]]
641
- ]]
642
- ];
643
-
644
- armature(animations=anim_data) {
645
- bone(name="Core", t=[0, 0, 0], r=[0, 0, 0]) {
646
- color([1.0, 0.85, 0.2], alpha=1.0, metalness=0.1, roughness=0.1, emissive=[1.0, 0.8, 0.1], emissiveIntensity=4.0)
647
- sphere(r=0.55);
648
-
649
- color([1.0, 1.0, 1.0], alpha=1.0, metalness=0.0, roughness=0.0, emissive=[1.0, 1.0, 1.0], emissiveIntensity=5.0)
650
- sphere(r=0.3);
651
-
652
- bone(name="Rings", t=[0, 0, 0], r=[0, 0, 0]) {
653
- color([0.2, 0.9, 1.0], alpha=0.9, metalness=0.8, roughness=0.2, emissive=[0.1, 0.8, 1.0], emissiveIntensity=2.5) {
654
- rotate([45, 0, 0])
655
- difference() {
656
- cylinder(h=0.08, r=0.95, center=true);
657
- cylinder(h=0.12, r=0.75, center=true);
658
- }
659
- rotate([0, 45, 0])
660
- difference() {
661
- cylinder(h=0.08, r=0.95, center=true);
662
- cylinder(h=0.75, r=0.75, center=true);
663
- }
664
- }
665
- }
666
- }
667
- }
668
- `,
669
- );
670
-
671
- // assets/projectile.scad
672
- writeFile(
673
- "assets/projectile.scad",
674
- `
675
- $fn = 16;
676
- $asa = 35.0;
677
-
678
- union() {
679
- translate([0, 0.8, 0])
680
- color([0.2, 1.0, 0.5], metalness=0.1, roughness=0.2, emissive=[0.2, 1.0, 0.5], emissiveIntensity=3.5)
681
- rotate([-90, 0, 0])
682
- cylinder(h=1.0, r1=0.25, r2=0.0, center=false);
683
-
684
- translate([0, 0, 0])
685
- color([0.8, 0.9, 1.0], metalness=0.9, roughness=0.2, emissive=[0.1, 0.4, 0.8], emissiveIntensity=2.0)
686
- rotate([-90, 0, 0])
687
- cylinder(h=1.4, r=0.12, center=true);
688
-
689
- translate([0, -0.6, 0])
690
- color([0.1, 0.7, 1.0], metalness=0.5, roughness=0.3, emissive=[0.1, 0.8, 1.0], emissiveIntensity=2.5) {
691
- cube([0.6, 0.3, 0.05], center=true);
692
- cube([0.05, 0.3, 0.6], center=true);
693
- }
694
- }
695
- `,
696
- );
697
-
698
- // =========================================================================
699
- // 2. GODOT ADDON: SCAD IMPORTER
700
- // =========================================================================
701
-
702
- writeFile(
703
- "addons/scad_importer/plugin.cfg",
704
- `
705
- [plugin]
706
-
707
- name="OpenSCAD GLTF Importer"
708
- description="Imports .scad files directly as 3D scenes using scad-gltf"
709
- author="Ilia Grigorev"
710
- version="0.1"
711
- script="scad_plugin.gd"
712
- `,
713
- );
714
-
715
- writeFile(
716
- "addons/scad_importer/scad_plugin.gd",
717
- `
718
- @tool
719
- extends EditorPlugin
720
-
721
- var import_plugin
722
-
723
- func _enter_tree():
724
- import_plugin = preload("res://addons/scad_importer/scad_importer.gd").new()
725
- add_scene_format_importer_plugin(import_plugin)
726
-
727
- func _exit_tree():
728
- remove_scene_format_importer_plugin(import_plugin)
729
- import_plugin = null
730
- `,
731
- );
732
-
733
- writeFile(
734
- "addons/scad_importer/scad_importer.gd",
735
- `
736
- @tool
737
- extends EditorSceneFormatImporter
738
-
739
- func _get_extensions():
740
- return PackedStringArray(["scad"])
741
-
742
- func _get_import_flags():
743
- return EditorSceneFormatImporter.IMPORT_SCENE
744
-
745
- func _import_scene(path: String, flags: int, options: Dictionary) -> Object:
746
- var global_source = ProjectSettings.globalize_path(path)
747
- var unique_id = str(hash(path))
748
- var temp_glb_path = ProjectSettings.globalize_path("user://scad_cache_" + unique_id + ".glb")
749
-
750
- var args = PackedStringArray()
751
- args.append(global_source)
752
- args.append(temp_glb_path)
753
-
754
- var output = []
755
- print("Importing %s via scad-convert..." % path.get_file())
756
-
757
- var exit_code = -1
758
- if OS.get_name() == "Windows":
759
- var win_args = PackedStringArray(["/c", "scad-convert"])
760
- win_args.append_array(args)
761
- exit_code = OS.execute("cmd.exe", win_args, output, true)
762
- else:
763
- exit_code = OS.execute("scad-convert", args, output, true)
764
-
765
- if exit_code != 0:
766
- print("scad-convert conversion failed for %s. Attempting fallback to local scad-serve..." % path.get_file())
767
- var fallback_success = _try_scad_serve_fallback(global_source, temp_glb_path)
768
-
769
- if not fallback_success:
770
- push_error("Failed to compile SCAD file: %s." % path.get_file())
771
- return null
772
-
773
- var gltf_doc = GLTFDocument.new()
774
- var gltf_state = GLTFState.new()
775
- var err = gltf_doc.append_from_file(temp_glb_path, gltf_state)
776
-
777
- if FileAccess.file_exists(temp_glb_path):
778
- DirAccess.remove_absolute(temp_glb_path)
779
-
780
- if err != OK:
781
- push_error("Failed to parse the generated GLB for %s." % path.get_file())
782
- return null
783
-
784
- var generated_scene = gltf_doc.generate_scene(gltf_state)
785
- if generated_scene:
786
- generated_scene.name = path.get_file().get_basename()
787
- _make_all_animations_loop(generated_scene)
788
-
789
- return generated_scene
790
-
791
- func _make_all_animations_loop(node: Node) -> void:
792
- if node is AnimationPlayer:
793
- for anim_name in node.get_animation_list():
794
- var anim = node.get_animation(anim_name)
795
- if anim:
796
- anim.loop_mode = Animation.LOOP_LINEAR
797
- for child in node.get_children():
798
- _make_all_animations_loop(child)
799
-
800
- func _get_relative_path(base: String, target: String) -> String:
801
- var base_parts = base.replace("\\\\", "/").split("/", false)
802
- var target_parts = target.replace("\\\\", "/").split("/", false)
803
-
804
- if OS.get_name() == "Windows":
805
- if base_parts.size() > 0 and target_parts.size() > 0:
806
- if base_parts[0].nocasecmp_to(target_parts[0]) != 0:
807
- return target
808
-
809
- var common_count = 0
810
- var min_len = min(base_parts.size(), target_parts.size())
811
- for i in range(min_len):
812
- if base_parts[i].nocasecmp_to(target_parts[i]) == 0:
813
- common_count += 1
814
- else:
815
- break
816
-
817
- var rel_parts = PackedStringArray()
818
- for i in range(common_count, base_parts.size()):
819
- rel_parts.append("..")
820
-
821
- for i in range(common_count, target_parts.size()):
822
- rel_parts.append(target_parts[i])
823
-
824
- return "/".join(rel_parts)
825
-
826
- func _get_dependencies_recursive(file_path: String, visited: Dictionary) -> void:
827
- if visited.has(file_path):
828
- return
829
-
830
- visited[file_path] = ""
831
-
832
- if not FileAccess.file_exists(file_path):
833
- return
834
-
835
- var file = FileAccess.open(file_path, FileAccess.READ)
836
- if not file:
837
- return
838
-
839
- var content = file.get_as_text()
840
- file.close()
841
-
842
- visited[file_path] = content
843
-
844
- var regex = RegEx.new()
845
- regex.compile("(?:include|use)\\\\s*[<\\"]([^>\\"]+)[>\\"]")
846
-
847
- var base_dir = file_path.get_base_dir()
848
- for result in regex.search_all(content):
849
- var dep_rel_path = result.get_string(1)
850
- var dep_abs_path = base_dir.path_join(dep_rel_path).simplify_path()
851
- _get_dependencies_recursive(dep_abs_path, visited)
852
-
853
- func _get_dependencies(file_path: String) -> Dictionary:
854
- var visited = {}
855
- _get_dependencies_recursive(file_path, visited)
856
- return visited
857
-
858
- func _try_scad_serve_fallback(source_path: String, out_glb_path: String) -> bool:
859
- var deps = _get_dependencies(source_path)
860
- var content = deps.get(source_path, "")
861
- deps.erase(source_path)
862
-
863
- if content == "":
864
- return false
865
-
866
- var additional_files = {}
867
- var base_dir = source_path.get_base_dir()
868
- for dep_path in deps.keys():
869
- var rel_path = _get_relative_path(base_dir, dep_path)
870
- additional_files[rel_path] = deps[dep_path]
871
-
872
- var http = HTTPClient.new()
873
- var err = http.connect_to_host("127.0.0.1", 3000)
874
- if err != OK:
875
- return false
876
-
877
- var max_wait = 500
878
- var wait = 0
879
- while http.get_status() in [HTTPClient.STATUS_CONNECTING, HTTPClient.STATUS_RESOLVING]:
880
- http.poll()
881
- OS.delay_msec(10)
882
- wait += 1
883
- if wait > max_wait:
884
- return false
885
-
886
- if http.get_status() != HTTPClient.STATUS_CONNECTED:
887
- return false
888
-
889
- var headers = PackedStringArray(["Content-Type: application/json"])
890
- var payload = {
891
- "content": content,
892
- "options": {
893
- "additionalFiles": additional_files
894
- }
895
- }
896
-
897
- var body = JSON.stringify(payload)
898
- err = http.request(HTTPClient.METHOD_POST, "/api/convert", headers, body)
899
- if err != OK:
900
- return false
901
-
902
- max_wait = 6000
903
- wait = 0
904
- while http.get_status() == HTTPClient.STATUS_REQUESTING:
905
- http.poll()
906
- OS.delay_msec(10)
907
- wait += 1
908
- if wait > max_wait:
909
- return false
910
-
911
- if http.has_response() and http.get_response_code() == 200:
912
- var rb = PackedByteArray()
913
- while http.get_status() == HTTPClient.STATUS_BODY:
914
- http.poll()
915
- var chunk = http.read_response_body_chunk()
916
- if chunk.size() == 0:
917
- OS.delay_msec(10)
918
- else:
919
- rb.append_array(chunk)
920
-
921
- if rb.is_empty():
922
- return false
923
-
924
- var out_file = FileAccess.open(out_glb_path, FileAccess.WRITE)
925
- if not out_file:
926
- return false
927
- out_file.store_buffer(rb)
928
- out_file.close()
929
-
930
- print("Successfully compiled %s using scad-serve fallback." % source_path.get_file())
931
- return true
932
-
933
- return false
934
- `,
935
- );
936
-
937
- // =========================================================================
938
- // 3. GDSCRIPT FILES
939
- // =========================================================================
940
-
941
- // scripts/spatial_grid.gd - FAST 2D COLLISION/SEPARATION CHECK
942
- writeFile(
943
- "scripts/spatial_grid.gd",
944
- `
945
- extends Node
946
-
947
- const CELL_SIZE: float = 2.5
948
- var grid: Dictionary = {}
949
- var active_cells: Array[Vector2i] = []
950
-
951
- func update_grid(enemies: Array):
952
- # Clear only previously active cells to prevent garbage generation in GDScript
953
- for c in active_cells:
954
- grid[c].clear()
955
- active_cells.clear()
956
-
957
- for e in enemies:
958
- if is_instance_valid(e):
959
- var cx = int(floor(e.global_position.x / CELL_SIZE))
960
- var cz = int(floor(e.global_position.z / CELL_SIZE))
961
- var cell = Vector2i(cx, cz)
962
-
963
- if not grid.has(cell):
964
- grid[cell] = []
965
-
966
- # If the list is empty, we register it to active_cells for clearing next frame
967
- if grid[cell].is_empty():
968
- active_cells.append(cell)
969
-
970
- grid[cell].append(e)
971
-
972
- func get_nearby(pos: Vector3) -> Array:
973
- var cx = int(floor(pos.x / CELL_SIZE))
974
- var cz = int(floor(pos.z / CELL_SIZE))
975
- var neighbors = []
976
-
977
- for dx in range(-1, 2):
978
- for dz in range(-1, 2):
979
- var cell = Vector2i(cx + dx, cz + dz)
980
- if grid.has(cell):
981
- neighbors.append_array(grid[cell])
982
-
983
- return neighbors
984
- `,
985
- );
986
-
987
- // scripts/sound_manager.gd
988
- writeFile(
989
- "scripts/sound_manager.gd",
990
- `
991
- extends Node
992
-
993
- var players: Array[AudioStreamPlayer] = []
994
- const POOL_SIZE = 16
995
-
996
- # CRITICAL FIX: Caching the generated AudioStreamWAV streams so we don't
997
- # calculate loops of thousands of iterations of math per-frame on large hits!
998
- var cached_streams: Dictionary = {}
999
-
1000
- func _ready():
1001
- process_mode = Node.PROCESS_MODE_ALWAYS
1002
- for i in range(POOL_SIZE):
1003
- var p = AudioStreamPlayer.new()
1004
- add_child(p)
1005
- players.append(p)
1006
-
1007
- func _get_free_player() -> AudioStreamPlayer:
1008
- for p in players:
1009
- if not p.playing:
1010
- return p
1011
- return players[0]
1012
-
1013
- func play_shoot():
1014
- _play_cached_tone("shoot", 580.0, 220.0, 0.12, 0.15, "saw")
1015
-
1016
- func play_hit():
1017
- _play_cached_tone("hit", 180.0, 60.0, 0.08, 0.2, "noise")
1018
-
1019
- func play_gem():
1020
- _play_cached_tone("gem", 650.0, 980.0, 0.1, 0.12, "sine")
1021
-
1022
- func play_level_up():
1023
- _play_cached_arpeggio("levelup", [523.25, 659.25, 783.99, 1046.5], 0.08, 0.25)
1024
-
1025
- func play_lightning():
1026
- _play_cached_tone("lightning", 300.0, 70.0, 0.25, 0.35, "noise")
1027
-
1028
- func play_hurt():
1029
- _play_cached_tone("hurt", 150.0, 50.0, 0.2, 0.3, "square")
1030
-
1031
- func _play_cached_tone(id: String, freq_start: float, freq_end: float, duration: float, volume: float, waveform: String):
1032
- if not cached_streams.has(id):
1033
- cached_streams[id] = _generate_tone(freq_start, freq_end, duration, volume, waveform)
1034
-
1035
- var player = _get_free_player()
1036
- player.stream = cached_streams[id]
1037
- player.play()
1038
-
1039
- func _play_cached_arpeggio(id: String, freqs: Array, note_duration: float, volume: float):
1040
- if not cached_streams.has(id):
1041
- cached_streams[id] = _generate_arpeggio(freqs, note_duration, volume)
1042
-
1043
- var player = _get_free_player()
1044
- player.stream = cached_streams[id]
1045
- player.play()
1046
-
1047
- func _generate_tone(freq_start: float, freq_end: float, duration: float, volume: float, waveform: String) -> AudioStreamWAV:
1048
- var sample_rate = 22050
1049
- var total_samples = int(duration * sample_rate)
1050
- var buffer = PackedByteArray()
1051
- buffer.resize(total_samples * 2)
1052
-
1053
- for i in range(total_samples):
1054
- var t = float(i) / float(total_samples)
1055
- var freq = lerp(freq_start, freq_end, t)
1056
- var phase = float(i) * freq / float(sample_rate)
1057
- var amp = (1.0 - t) * volume
1058
- var sample = 0.0
1059
-
1060
- match waveform:
1061
- "sine":
1062
- sample = sin(phase * TAU) * amp
1063
- "square":
1064
- sample = (1.0 if sin(phase * TAU) > 0.0 else -1.0) * amp
1065
- "saw":
1066
- sample = (fmod(phase, 1.0) * 2.0 - 1.0) * amp
1067
- "noise":
1068
- sample = randf_range(-amp, amp)
1069
-
1070
- var val16 = int(clamp(sample, -1.0, 1.0) * 32767.0)
1071
- buffer.encode_s16(i * 2, val16)
1072
-
1073
- var stream = AudioStreamWAV.new()
1074
- stream.format = AudioStreamWAV.FORMAT_16_BITS
1075
- stream.mix_rate = sample_rate
1076
- stream.stereo = false
1077
- stream.data = buffer
1078
- return stream
1079
-
1080
- func _generate_arpeggio(freqs: Array, note_duration: float, volume: float) -> AudioStreamWAV:
1081
- var sample_rate = 22050
1082
- var note_samples = int(note_duration * sample_rate)
1083
- var total_samples = note_samples * freqs.size()
1084
- var buffer = PackedByteArray()
1085
- buffer.resize(total_samples * 2)
1086
-
1087
- for note_idx in range(freqs.size()):
1088
- var freq = freqs[note_idx]
1089
- for i in range(note_samples):
1090
- var idx = note_idx * note_samples + i
1091
- var t = float(i) / float(note_samples)
1092
- var phase = float(i) * freq / float(sample_rate)
1093
- var amp = (1.0 - t * 0.5) * volume
1094
- var sample = sin(phase * TAU) * amp
1095
- var val16 = int(clamp(sample, -1.0, 1.0) * 32767.0)
1096
- buffer.encode_s16(idx * 2, val16)
1097
-
1098
- var stream = AudioStreamWAV.new()
1099
- stream.format = AudioStreamWAV.FORMAT_16_BITS
1100
- stream.mix_rate = sample_rate
1101
- stream.stereo = false
1102
- stream.data = buffer
1103
- return stream
1104
- `,
1105
- );
1106
-
1107
- // scripts/damage_number.gd
1108
- writeFile(
1109
- "scripts/damage_number.gd",
1110
- `
1111
- extends Node3D
1112
-
1113
- @onready var label: Label3D = $Label3D
1114
- var velocity: Vector3 = Vector3(0, 3.5, 0)
1115
- var lifetime: float = 0.65
1116
-
1117
- func setup(amount: int, is_crit: bool = false):
1118
- label.text = str(amount)
1119
- if is_crit:
1120
- label.modulate = Color(1.0, 0.85, 0.1)
1121
- label.font_size = 42
1122
- velocity.y = 4.5
1123
- else:
1124
- label.modulate = Color(1.0, 0.3, 0.2)
1125
- label.font_size = 32
1126
-
1127
- velocity.x = randf_range(-1.5, 1.5)
1128
- velocity.z = randf_range(-1.5, 1.5)
1129
-
1130
- func _process(delta: float):
1131
- global_position += velocity * delta
1132
- velocity.y -= 5.0 * delta
1133
- lifetime -= delta
1134
- var alpha = clamp(lifetime / 0.65, 0.0, 1.0)
1135
- label.modulate.a = alpha
1136
- if lifetime <= 0:
1137
- queue_free()
1138
- `,
1139
- );
1140
-
1141
- // scenes/damage_number.tscn
1142
- writeFile(
1143
- "scenes/damage_number.tscn",
1144
- `
1145
- [gd_scene load_steps=2 format=3 uid="uid://dmgnum001"]
1146
-
1147
- [ext_resource type="Script" path="res://scripts/damage_number.gd" id="1_dnum"]
1148
-
1149
- [node name="DamageNumber" type="Node3D"]
1150
- script = ExtResource("1_dnum")
1151
-
1152
- [node name="Label3D" type="Label3D" parent="."]
1153
- billboard = 1
1154
- no_depth_test = true
1155
- font_size = 32
1156
- outline_size = 8
1157
- outline_modulate = Color(0, 0, 0, 1)
1158
- text = "10"
1159
- `,
1160
- );
1161
-
1162
- // scripts/xp_gem.gd
1163
- writeFile(
1164
- "scripts/xp_gem.gd",
1165
- `
1166
- extends Area3D
1167
-
1168
- @export var xp_value: int = 10
1169
- var player: CharacterBody3D = null
1170
- var magnet_speed: float = 0.0
1171
- var collected: bool = false
1172
-
1173
- func _ready():
1174
- add_to_group("gems")
1175
- _setup_loop_anim(self)
1176
-
1177
- func _setup_loop_anim(node: Node):
1178
- if node is AnimationPlayer:
1179
- for anim_name in node.get_animation_list():
1180
- var anim = node.get_animation(anim_name)
1181
- if anim:
1182
- anim.loop_mode = Animation.LOOP_LINEAR
1183
- if node.get_animation_list().size() > 0:
1184
- node.play(node.get_animation_list()[0])
1185
- var a = node.get_animation(node.get_animation_list()[0])
1186
- if a:
1187
- node.seek(randf_range(0.0, a.length), true)
1188
- return
1189
- for child in node.get_children():
1190
- _setup_loop_anim(child)
1191
-
1192
- func _process(delta: float):
1193
- rotation.y += 2.5 * delta
1194
-
1195
- if player and is_instance_valid(player):
1196
- magnet_speed += 40.0 * delta
1197
- var dir = (player.global_position + Vector3(0, 1.0, 0) - global_position).normalized()
1198
- global_position += dir * magnet_speed * delta
1199
-
1200
- if global_position.distance_to(player.global_position + Vector3(0, 1.0, 0)) < 0.8:
1201
- _collect()
1202
-
1203
- func attract_to(target: CharacterBody3D):
1204
- player = target
1205
-
1206
- func _collect():
1207
- if collected:
1208
- return
1209
- collected = true
1210
- SoundManager.play_gem()
1211
- if player and player.has_method("add_xp"):
1212
- player.add_xp(xp_value)
1213
- queue_free()
1214
- `,
1215
- );
1216
-
1217
- // scenes/xp_gem.tscn
1218
- writeFile(
1219
- "scenes/xp_gem.tscn",
1220
- `
1221
- [gd_scene load_steps=4 format=3 uid="uid://gem0001"]
1222
-
1223
- [ext_resource type="Script" path="res://scripts/xp_gem.gd" id="1_gem"]
1224
- [ext_resource type="PackedScene" path="res://assets/gem_xp.scad" id="2_gem_mesh"]
1225
-
1226
- [sub_resource type="SphereShape3D" id="SphereShape3D_1"]
1227
- radius = 0.8
1228
-
1229
- [node name="XPGem" type="Area3D"]
1230
- collision_layer = 8
1231
- collision_mask = 2
1232
- script = ExtResource("1_gem")
1233
-
1234
- [node name="Model" parent="." instance=ExtResource("2_gem_mesh")]
1235
- transform = Transform3D(0.6, 0, 0, 0, 0.6, 0, 0, 0, 0.6, 0, 0, 0)
1236
-
1237
- [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
1238
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.4, 0)
1239
- shape = SubResource("SphereShape3D_1")
1240
- `,
1241
- );
1242
-
1243
- // scripts/projectile.gd
1244
- writeFile(
1245
- "scripts/projectile.gd",
1246
- `
1247
- extends Area3D
1248
-
1249
- var direction: Vector3 = Vector3.FORWARD
1250
- var speed: float = 24.0
1251
- var damage: int = 25
1252
- var pierce: int = 1
1253
- var lifetime: float = 3.0
1254
-
1255
- func setup(dir: Vector3, dmg: int, p_speed: float = 24.0, p_pierce: int = 1):
1256
- direction = dir.normalized()
1257
- damage = dmg
1258
- speed = p_speed
1259
- pierce = p_pierce
1260
- if direction != Vector3.ZERO:
1261
- look_at(global_position + direction, Vector3.UP)
1262
-
1263
- func _process(delta: float):
1264
- global_position += direction * speed * delta
1265
- lifetime -= delta
1266
- if lifetime <= 0.0:
1267
- queue_free()
1268
-
1269
- func _on_body_entered(body: Node3D):
1270
- if body.is_in_group("enemies") and body.has_method("take_damage"):
1271
- body.take_damage(damage, global_position)
1272
- pierce -= 1
1273
- if pierce <= 0:
1274
- queue_free()
1275
- `,
1276
- );
1277
-
1278
- // scenes/projectile.tscn
1279
- writeFile(
1280
- "scenes/projectile.tscn",
1281
- `
1282
- [gd_scene load_steps=4 format=3 uid="uid://proj0001"]
1283
-
1284
- [ext_resource type="Script" path="res://scripts/projectile.gd" id="1_proj"]
1285
- [ext_resource type="PackedScene" path="res://assets/projectile.scad" id="2_proj_mesh"]
1286
-
1287
- [sub_resource type="SphereShape3D" id="SphereShape3D_1"]
1288
- radius = 0.55
1289
-
1290
- [node name="Projectile" type="Area3D"]
1291
- collision_layer = 16
1292
- collision_mask = 4
1293
- script = ExtResource("1_proj")
1294
-
1295
- [node name="Model" parent="." instance=ExtResource("2_proj_mesh")]
1296
- transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 0, 0, 0)
1297
-
1298
- [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
1299
- shape = SubResource("SphereShape3D_1")
1300
-
1301
- [connection signal="body_entered" from="." to="." method="_on_body_entered"]
1302
- `,
1303
- );
1304
-
1305
- // scripts/orbit_shield.gd
1306
- writeFile(
1307
- "scripts/orbit_shield.gd",
1308
- `
1309
- extends Node3D
1310
-
1311
- @export var damage: int = 30
1312
- @export var rotation_speed: float = 3.0
1313
- var current_angle: float = 0.0
1314
- var radius: float = 3.2
1315
- var orb_count: int = 1
1316
- var orb_scene = preload("res://scenes/orbit_orb.tscn")
1317
- var orbs: Array[Node3D] = []
1318
-
1319
- func _ready():
1320
- update_orbs()
1321
-
1322
- func set_orb_count(count: int):
1323
- orb_count = count
1324
- update_orbs()
1325
-
1326
- func update_orbs():
1327
- for o in orbs:
1328
- if is_instance_valid(o):
1329
- o.queue_free()
1330
- orbs.clear()
1331
-
1332
- for i in range(orb_count):
1333
- var orb = orb_scene.instantiate()
1334
- add_child(orb)
1335
- orb.damage = damage
1336
- orbs.append(orb)
1337
-
1338
- func _process(delta: float):
1339
- current_angle += rotation_speed * delta
1340
- for i in range(orbs.size()):
1341
- var angle = current_angle + (TAU / orbs.size()) * i
1342
- var pos = Vector3(cos(angle) * radius, 1.2, sin(angle) * radius)
1343
- orbs[i].position = pos
1344
- `,
1345
- );
1346
-
1347
- // scripts/orbit_orb.gd
1348
- writeFile(
1349
- "scripts/orbit_orb.gd",
1350
- `
1351
- extends Area3D
1352
-
1353
- var damage: int = 30
1354
- var hit_cooldowns: Dictionary = {}
1355
-
1356
- func _ready():
1357
- _setup_loop_anim(self)
1358
-
1359
- func _setup_loop_anim(node: Node):
1360
- if node is AnimationPlayer:
1361
- for anim_name in node.get_animation_list():
1362
- var anim = node.get_animation(anim_name)
1363
- if anim:
1364
- anim.loop_mode = Animation.LOOP_LINEAR
1365
- if node.get_animation_list().size() > 0:
1366
- node.play(node.get_animation_list()[0])
1367
- return
1368
- for child in node.get_children():
1369
- _setup_loop_anim(child)
1370
-
1371
- func _process(delta: float):
1372
- var to_remove = []
1373
- for e in hit_cooldowns.keys():
1374
- hit_cooldowns[e] -= delta
1375
- if hit_cooldowns[e] <= 0:
1376
- to_remove.append(e)
1377
- for e in to_remove:
1378
- hit_cooldowns.erase(e)
1379
-
1380
- func _on_body_entered(body: Node3D):
1381
- if body.is_in_group("enemies") and body.has_method("take_damage"):
1382
- if not hit_cooldowns.has(body):
1383
- hit_cooldowns[body] = 0.35
1384
- body.take_damage(damage, global_position)
1385
- SoundManager.play_hit()
1386
- `,
1387
- );
1388
-
1389
- // scenes/orbit_orb.tscn
1390
- writeFile(
1391
- "scenes/orbit_orb.tscn",
1392
- `
1393
- [gd_scene load_steps=4 format=3 uid="uid://orb0001"]
1394
-
1395
- [ext_resource type="Script" path="res://scripts/orbit_orb.gd" id="1_orb"]
1396
- [ext_resource type="PackedScene" path="res://assets/orb.scad" id="2_orb_mesh"]
1397
-
1398
- [sub_resource type="SphereShape3D" id="SphereShape3D_1"]
1399
- radius = 0.85
1400
-
1401
- [node name="OrbitOrb" type="Area3D"]
1402
- collision_layer = 16
1403
- collision_mask = 4
1404
- script = ExtResource("1_orb")
1405
-
1406
- [node name="Model" parent="." instance=ExtResource("2_orb_mesh")]
1407
- transform = Transform3D(0.7, 0, 0, 0, 0.7, 0, 0, 0, 0.7, 0, 0, 0)
1408
-
1409
- [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
1410
- shape = SubResource("SphereShape3D_1")
1411
-
1412
- [connection signal="body_entered" from="." to="." method="_on_body_entered"]
1413
- `,
1414
- );
1415
-
1416
- // scripts/lightning_strike.gd
1417
- writeFile(
1418
- "scripts/lightning_strike.gd",
1419
- `
1420
- extends Node3D
1421
-
1422
- @onready var mesh: MeshInstance3D = $MeshInstance3D
1423
- var lifetime: float = 0.25
1424
-
1425
- func _ready():
1426
- SoundManager.play_lightning()
1427
-
1428
- func _process(delta: float):
1429
- lifetime -= delta
1430
- var a = lifetime / 0.25
1431
- scale.x = a
1432
- scale.z = a
1433
- if lifetime <= 0:
1434
- queue_free()
1435
- `,
1436
- );
1437
-
1438
- // scenes/lightning_strike.tscn
1439
- writeFile(
1440
- "scenes/lightning_strike.tscn",
1441
- `
1442
- [gd_scene load_steps=4 format=3 uid="uid://lightn001"]
1443
-
1444
- [ext_resource type="Script" path="res://scripts/lightning_strike.gd" id="1_light"]
1445
-
1446
- [sub_resource type="CylinderMesh" id="CylinderMesh_1"]
1447
- top_radius = 0.3
1448
- bottom_radius = 0.8
1449
- height = 20.0
1450
-
1451
- [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
1452
- transparency = 1
1453
- albedo_color = Color(0.6, 0.9, 1, 0.9)
1454
- emission_enabled = true
1455
- emission = Color(0.3, 0.8, 1, 1)
1456
- emission_energy_multiplier = 4.0
1457
-
1458
- [node name="LightningStrike" type="Node3D"]
1459
- script = ExtResource("1_light")
1460
-
1461
- [node name="MeshInstance3D" type="MeshInstance3D" parent="."]
1462
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 10, 0)
1463
- mesh = SubResource("CylinderMesh_1")
1464
- surface_material_override/0 = SubResource("StandardMaterial3D_1")
1465
- `,
1466
- );
1467
-
1468
- // scripts/enemy_base.gd
1469
- writeFile(
1470
- "scripts/enemy_base.gd",
1471
- `
1472
- extends CharacterBody3D
1473
-
1474
- @export var max_health: int = 40
1475
- @export var speed: float = 3.8
1476
- @export var damage: int = 10
1477
- @export var xp_amount: int = 15
1478
- @export var is_boss: bool = false
1479
- @export var target_height: float = 0.0
1480
- @export var separation_radius: float = 1.5
1481
-
1482
- var health: int
1483
- var player: CharacterBody3D = null
1484
- var flash_timer: float = 0.0
1485
- var gem_scene = preload("res://scenes/xp_gem.tscn")
1486
- var dmg_num_scene = preload("res://scenes/damage_number.tscn")
1487
-
1488
- @onready var model_container = $ModelContainer
1489
- var anim_player: AnimationPlayer = null
1490
- var active_anim_name: String = ""
1491
-
1492
- # CRITICAL FIX: Caching the StandardMaterial3D per enemy instance prevents standard material
1493
- # re-instantiation spam every time damage is taken (which chokes up garbage collection)
1494
- var flash_mat: StandardMaterial3D
1495
-
1496
- func _ready():
1497
- health = max_health
1498
- add_to_group("enemies")
1499
- player = get_tree().get_first_node_in_group("player")
1500
-
1501
- motion_mode = CharacterBody3D.MOTION_MODE_FLOATING
1502
- axis_lock_linear_y = true
1503
- global_position.y = target_height
1504
-
1505
- flash_mat = StandardMaterial3D.new()
1506
- flash_mat.albedo_color = Color(1.0, 0.2, 0.2)
1507
- flash_mat.emission_enabled = true
1508
- flash_mat.emission = Color(1.0, 0.1, 0.1)
1509
- flash_mat.emission_energy_multiplier = 3.0
1510
-
1511
- _setup_looping_animations(self)
1512
-
1513
- func _setup_looping_animations(node: Node):
1514
- if node is AnimationPlayer:
1515
- anim_player = node
1516
- _configure_and_play()
1517
- return
1518
- for child in node.get_children():
1519
- _setup_looping_animations(child)
1520
- if anim_player:
1521
- return
1522
-
1523
- func _configure_and_play():
1524
- if not anim_player:
1525
- return
1526
-
1527
- var anim_list = anim_player.get_animation_list()
1528
- if anim_list.is_empty():
1529
- return
1530
-
1531
- for a_name in anim_list:
1532
- var anim = anim_player.get_animation(a_name)
1533
- if anim:
1534
- anim.loop_mode = Animation.LOOP_LINEAR
1535
-
1536
- var chosen_anim = anim_list[0]
1537
- for a_name in anim_list:
1538
- var lower = a_name.to_lower()
1539
- if "walk" in lower or "fly" in lower:
1540
- chosen_anim = a_name
1541
- break
1542
-
1543
- active_anim_name = chosen_anim
1544
- anim_player.play(active_anim_name)
1545
-
1546
- var a = anim_player.get_animation(active_anim_name)
1547
- if a and a.length > 0.0:
1548
- anim_player.seek(randf_range(0.0, a.length), true)
1549
-
1550
- func _physics_process(delta: float):
1551
- global_position.y = target_height
1552
- velocity.y = 0.0
1553
-
1554
- if flash_timer > 0:
1555
- flash_timer -= delta
1556
- if flash_timer <= 0:
1557
- _reset_material()
1558
-
1559
- if anim_player and not active_anim_name.is_empty():
1560
- if not anim_player.is_playing():
1561
- anim_player.play(active_anim_name)
1562
-
1563
- if not player or not is_instance_valid(player):
1564
- player = get_tree().get_first_node_in_group("player")
1565
- return
1566
-
1567
- var diff = player.global_position - global_position
1568
- diff.y = 0
1569
- var dir = diff.normalized()
1570
-
1571
- # --- 2D GRID FAST CHECK SEPARATION ---
1572
- var separation := Vector3.ZERO
1573
- var neighbors = SpatialGrid.get_nearby(global_position)
1574
- var overlap_count = 0
1575
-
1576
- for n in neighbors:
1577
- if n != self and is_instance_valid(n):
1578
- var dist = global_position.distance_to(n.global_position)
1579
- if dist < separation_radius:
1580
- var push: Vector3
1581
- if dist < 0.001:
1582
- push = Vector3(randf_range(-1.0, 1.0), 0, randf_range(-1.0, 1.0)).normalized()
1583
- dist = 0.01
1584
- else:
1585
- push = (global_position - n.global_position).normalized()
1586
- push.y = 0
1587
-
1588
- separation += push * (separation_radius - dist)
1589
- overlap_count += 1
1590
-
1591
- if overlap_count > 0:
1592
- separation /= float(overlap_count)
1593
- dir = (dir + separation * 3.0).normalized()
1594
- # -------------------------------------
1595
-
1596
- velocity.x = dir.x * speed
1597
- velocity.z = dir.z * speed
1598
- move_and_slide()
1599
-
1600
- global_position.y = target_height
1601
- velocity.y = 0.0
1602
-
1603
- if dir.length_squared() > 0.001:
1604
- var target_yaw = atan2(-dir.x, -dir.z)
1605
- rotation.y = lerp_angle(rotation.y, target_yaw, 10.0 * delta)
1606
-
1607
- for i in range(get_slide_collision_count()):
1608
- var col = get_slide_collision(i)
1609
- var collider = col.get_collider()
1610
- if collider and collider.is_in_group("player") and collider.has_method("take_damage"):
1611
- collider.take_damage(damage)
1612
-
1613
- func take_damage(amount: int, from_pos: Vector3 = Vector3.ZERO):
1614
- health -= amount
1615
- SoundManager.play_hit()
1616
-
1617
- var dmg = dmg_num_scene.instantiate()
1618
- get_parent().add_child(dmg)
1619
- dmg.global_position = global_position + Vector3(0, 1.8, 0)
1620
- dmg.setup(amount, amount > 35)
1621
-
1622
- if from_pos != Vector3.ZERO:
1623
- var knock_dir = (global_position - from_pos).normalized()
1624
- knock_dir.y = 0
1625
- global_position += knock_dir * 0.45
1626
- global_position.y = target_height
1627
-
1628
- _flash_red()
1629
-
1630
- if health <= 0:
1631
- _die()
1632
-
1633
- func _flash_red():
1634
- flash_timer = 0.1
1635
- _apply_flash(model_container)
1636
-
1637
- func _apply_flash(node: Node):
1638
- if node is MeshInstance3D:
1639
- node.material_override = flash_mat
1640
- for child in node.get_children():
1641
- _apply_flash(child)
1642
-
1643
- func _reset_material():
1644
- _clear_flash(model_container)
1645
-
1646
- func _clear_flash(node: Node):
1647
- if node is MeshInstance3D:
1648
- node.material_override = null
1649
- for child in node.get_children():
1650
- _clear_flash(child)
1651
-
1652
- func _die():
1653
- var gem = gem_scene.instantiate()
1654
- gem.xp_value = xp_amount
1655
- get_parent().add_child(gem)
1656
- gem.global_position = global_position + Vector3(0, 0.5, 0)
1657
-
1658
- if is_boss:
1659
- for i in range(4):
1660
- var extra_gem = gem_scene.instantiate()
1661
- extra_gem.xp_value = xp_amount
1662
- get_parent().add_child(extra_gem)
1663
- extra_gem.global_position = global_position + Vector3(randf_range(-1.5, 1.5), 0.5, randf_range(-1.5, 1.5))
1664
-
1665
- var main = get_tree().get_first_node_in_group("main")
1666
- if main and main.has_method("register_kill"):
1667
- main.register_kill()
1668
-
1669
- queue_free()
1670
- `,
1671
- );
1672
-
1673
- // scenes/enemy_walker.tscn
1674
- // Physics Engine collision_mask 3 (World + Player) to ignore Godot rigid separation.
1675
- writeFile(
1676
- "scenes/enemy_walker.tscn",
1677
- `
1678
- [gd_scene load_steps=4 format=3 uid="uid://enmwalker01"]
1679
-
1680
- [ext_resource type="Script" path="res://scripts/enemy_base.gd" id="1_ebase"]
1681
- [ext_resource type="PackedScene" path="res://assets/enemy_walker.scad" id="2_mesh"]
1682
-
1683
- [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
1684
- radius = 0.65
1685
- height = 2.4
1686
-
1687
- [node name="EnemyWalker" type="CharacterBody3D"]
1688
- collision_layer = 4
1689
- collision_mask = 3
1690
- axis_lock_linear_y = true
1691
- motion_mode = 1
1692
- script = ExtResource("1_ebase")
1693
- max_health = 35
1694
- speed = 3.2
1695
- damage = 10
1696
- xp_amount = 12
1697
- target_height = 0.0
1698
- separation_radius = 1.3
1699
-
1700
- [node name="ModelContainer" type="Node3D" parent="."]
1701
-
1702
- [node name="Model" parent="ModelContainer" instance=ExtResource("2_mesh")]
1703
- transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 0, 0, 0)
1704
-
1705
- [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
1706
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0)
1707
- shape = SubResource("CapsuleShape3D_1")
1708
- `,
1709
- );
1710
-
1711
- // scenes/enemy_swarmer.tscn
1712
- writeFile(
1713
- "scenes/enemy_swarmer.tscn",
1714
- `
1715
- [gd_scene load_steps=4 format=3 uid="uid://enmswarm01"]
1716
-
1717
- [ext_resource type="Script" path="res://scripts/enemy_base.gd" id="1_ebase"]
1718
- [ext_resource type="PackedScene" path="res://assets/enemy_swarmer.scad" id="2_mesh"]
1719
-
1720
- [sub_resource type="SphereShape3D" id="SphereShape3D_1"]
1721
- radius = 0.8
1722
-
1723
- [node name="EnemySwarmer" type="CharacterBody3D"]
1724
- collision_layer = 4
1725
- collision_mask = 3
1726
- axis_lock_linear_y = true
1727
- motion_mode = 1
1728
- script = ExtResource("1_ebase")
1729
- max_health = 18
1730
- speed = 5.6
1731
- damage = 6
1732
- xp_amount = 8
1733
- target_height = 0.8
1734
- separation_radius = 1.2
1735
-
1736
- [node name="ModelContainer" type="Node3D" parent="."]
1737
-
1738
- [node name="Model" parent="ModelContainer" instance=ExtResource("2_mesh")]
1739
- transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, 0, 0, 0)
1740
-
1741
- [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
1742
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0)
1743
- shape = SubResource("SphereShape3D_1")
1744
- `,
1745
- );
1746
-
1747
- // scenes/enemy_brute.tscn
1748
- writeFile(
1749
- "scenes/enemy_brute.tscn",
1750
- `
1751
- [gd_scene load_steps=4 format=3 uid="uid://enmbrute01"]
1752
-
1753
- [ext_resource type="Script" path="res://scripts/enemy_base.gd" id="1_ebase"]
1754
- [ext_resource type="PackedScene" path="res://assets/enemy_brute.scad" id="2_mesh"]
1755
-
1756
- [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
1757
- radius = 1.4
1758
- height = 3.6
1759
-
1760
- [node name="EnemyBrute" type="CharacterBody3D"]
1761
- collision_layer = 4
1762
- collision_mask = 3
1763
- axis_lock_linear_y = true
1764
- motion_mode = 1
1765
- script = ExtResource("1_ebase")
1766
- max_health = 250
1767
- speed = 2.4
1768
- damage = 25
1769
- xp_amount = 80
1770
- is_boss = true
1771
- target_height = 0.0
1772
- separation_radius = 3.0
1773
-
1774
- [node name="ModelContainer" type="Node3D" parent="."]
1775
-
1776
- [node name="Model" parent="ModelContainer" instance=ExtResource("2_mesh")]
1777
- transform = Transform3D(1.1, 0, 0, 0, 1.1, 0, 0, 0, 1.1, 0, 0, 0)
1778
-
1779
- [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
1780
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8, 0)
1781
- shape = SubResource("CapsuleShape3D_1")
1782
- `,
1783
- );
1784
-
1785
- // scripts/player.gd
1786
- writeFile(
1787
- "scripts/player.gd",
1788
- `
1789
- extends CharacterBody3D
1790
-
1791
- signal health_changed(current, max_hp)
1792
- signal xp_changed(current, required, level)
1793
- signal leveled_up(new_level)
1794
- signal died()
1795
-
1796
- @export var move_speed: float = 7.0
1797
- @export var max_health: int = 100
1798
- var current_health: int = 100
1799
-
1800
- var level: int = 1
1801
- var current_xp: int = 0
1802
- var required_xp: int = 30
1803
- var magnet_radius: float = 4.5
1804
-
1805
- var projectile_cooldown: float = 0.85
1806
- var projectile_timer: float = 0.0
1807
- var projectile_damage: int = 30
1808
- var projectile_count: int = 1
1809
-
1810
- var lightning_unlocked: bool = false
1811
- var lightning_cooldown: float = 2.2
1812
- var lightning_timer: float = 0.0
1813
- var lightning_damage: int = 65
1814
-
1815
- var orbit_shield_unlocked: bool = true
1816
- var orbit_count: int = 1
1817
-
1818
- @onready var model: Node3D = $Model
1819
- @onready var magnet_area: Area3D = $MagnetArea
1820
- @onready var orbit_shield: Node3D = $OrbitShield
1821
-
1822
- var anim_player: AnimationPlayer = null
1823
- var walk_anim_name: String = ""
1824
-
1825
- var proj_scene = preload("res://scenes/projectile.tscn")
1826
- var lightning_scene = preload("res://scenes/lightning_strike.tscn")
1827
- var invulnerable_timer: float = 0.0
1828
-
1829
- # --- INVISIBLE TOUCH CONTROLS ---
1830
- var touch_index: int = -1
1831
- var touch_start_pos: Vector2 = Vector2.ZERO
1832
- var touch_input_vec: Vector2 = Vector2.ZERO
1833
- const TOUCH_DEADZONE: float = 8.0
1834
- const TOUCH_MAX_DIST: float = 65.0
1835
-
1836
- func _ready():
1837
- add_to_group("player")
1838
- current_health = max_health
1839
- emit_signal("health_changed", current_health, max_health)
1840
- emit_signal("xp_changed", current_xp, required_xp, level)
1841
-
1842
- motion_mode = CharacterBody3D.MOTION_MODE_FLOATING
1843
- axis_lock_linear_y = true
1844
- global_position.y = 0.0
1845
-
1846
- _update_magnet_shape()
1847
- orbit_shield.set_orb_count(orbit_count)
1848
- _setup_player_animations(model)
1849
-
1850
- func _unhandled_input(event: InputEvent) -> void:
1851
- if event is InputEventScreenTouch:
1852
- if event.pressed:
1853
- if touch_index == -1:
1854
- touch_index = event.index
1855
- touch_start_pos = event.position
1856
- touch_input_vec = Vector2.ZERO
1857
- elif event.index == touch_index or event.is_canceled():
1858
- _reset_touch()
1859
- elif event is InputEventScreenDrag:
1860
- if event.index == touch_index:
1861
- var diff = event.position - touch_start_pos
1862
- var dist = diff.length()
1863
- if dist > TOUCH_DEADZONE:
1864
- var move_dist = min(dist - TOUCH_DEADZONE, TOUCH_MAX_DIST)
1865
- touch_input_vec = diff.normalized() * (move_dist / TOUCH_MAX_DIST)
1866
- # Follow the finger smoothly if dragged past max threshold
1867
- if dist > TOUCH_MAX_DIST + TOUCH_DEADZONE:
1868
- touch_start_pos = event.position - diff.normalized() * (TOUCH_MAX_DIST + TOUCH_DEADZONE)
1869
- else:
1870
- touch_input_vec = Vector2.ZERO
1871
-
1872
- func _reset_touch():
1873
- touch_index = -1
1874
- touch_input_vec = Vector2.ZERO
1875
-
1876
- func _setup_player_animations(node: Node):
1877
- if node is AnimationPlayer:
1878
- anim_player = node
1879
- for a_name in anim_player.get_animation_list():
1880
- var anim = anim_player.get_animation(a_name)
1881
- if anim:
1882
- anim.loop_mode = Animation.LOOP_LINEAR
1883
- if "walk" in a_name.to_lower():
1884
- walk_anim_name = a_name
1885
- if walk_anim_name.is_empty() and anim_player.get_animation_list().size() > 0:
1886
- walk_anim_name = anim_player.get_animation_list()[0]
1887
- return
1888
- for child in node.get_children():
1889
- _setup_player_animations(child)
1890
- if anim_player:
1891
- return
1892
-
1893
- func _physics_process(delta: float):
1894
- global_position.y = 0.0
1895
- velocity.y = 0.0
1896
-
1897
- if invulnerable_timer > 0:
1898
- invulnerable_timer -= delta
1899
-
1900
- var input_vec = Vector2.ZERO
1901
- input_vec.x = Input.get_axis("move_left", "move_right")
1902
- input_vec.y = Input.get_axis("move_up", "move_down")
1903
-
1904
- # Blend or apply touch screen input
1905
- if touch_input_vec != Vector2.ZERO:
1906
- input_vec += touch_input_vec
1907
-
1908
- if input_vec.length() > 1.0:
1909
- input_vec = input_vec.normalized()
1910
-
1911
- var move_dir = Vector3(input_vec.x, 0, input_vec.y)
1912
- velocity.x = move_dir.x * move_speed
1913
- velocity.z = move_dir.z * move_speed
1914
- move_and_slide()
1915
-
1916
- global_position.y = 0.0
1917
- velocity.y = 0.0
1918
-
1919
- if move_dir.length_squared() > 0.01:
1920
- var target_yaw = atan2(-move_dir.x, -move_dir.z)
1921
- model.rotation.y = lerp_angle(model.rotation.y, target_yaw, 15.0 * delta)
1922
-
1923
- if anim_player and not walk_anim_name.is_empty():
1924
- if not anim_player.is_playing() or anim_player.current_animation != walk_anim_name:
1925
- anim_player.play(walk_anim_name)
1926
- else:
1927
- if anim_player and anim_player.is_playing() and anim_player.current_animation == walk_anim_name:
1928
- anim_player.stop()
1929
-
1930
- _process_weapons(delta)
1931
- _process_magnet()
1932
-
1933
- func _process_weapons(delta: float):
1934
- projectile_timer += delta
1935
- if projectile_timer >= projectile_cooldown:
1936
- projectile_timer = 0.0
1937
- _fire_projectiles()
1938
-
1939
- if lightning_unlocked:
1940
- lightning_timer += delta
1941
- if lightning_timer >= lightning_cooldown:
1942
- lightning_timer = 0.0
1943
- _strike_lightning()
1944
-
1945
- func _fire_projectiles():
1946
- var nearest_enemy = _get_nearest_enemy()
1947
- var base_dir = Vector3.FORWARD
1948
- if nearest_enemy:
1949
- base_dir = (nearest_enemy.global_position - global_position).normalized()
1950
- base_dir.y = 0
1951
- elif velocity.length_squared() > 0.01:
1952
- base_dir = velocity.normalized()
1953
- base_dir.y = 0
1954
-
1955
- for i in range(projectile_count):
1956
- var spread_angle = 0.0
1957
- if projectile_count > 1:
1958
- spread_angle = deg_to_rad(-15.0 + (30.0 / (projectile_count - 1)) * i)
1959
-
1960
- var rotated_dir = base_dir.rotated(Vector3.UP, spread_angle)
1961
- var proj = proj_scene.instantiate()
1962
- get_parent().add_child(proj)
1963
- proj.global_position = global_position + Vector3(0, 1.2, 0) + rotated_dir * 0.8
1964
- proj.setup(rotated_dir, projectile_damage)
1965
-
1966
- SoundManager.play_shoot()
1967
-
1968
- func _strike_lightning():
1969
- var enemies = get_tree().get_nodes_in_group("enemies")
1970
- if enemies.is_empty():
1971
- return
1972
-
1973
- enemies.shuffle()
1974
- var target = enemies[0]
1975
- if target and is_instance_valid(target):
1976
- var strike = lightning_scene.instantiate()
1977
- get_parent().add_child(strike)
1978
- strike.global_position = target.global_position
1979
-
1980
- for e in enemies:
1981
- if is_instance_valid(e) and e.global_position.distance_to(target.global_position) < 3.2:
1982
- e.take_damage(lightning_damage, target.global_position)
1983
-
1984
- func _get_nearest_enemy() -> CharacterBody3D:
1985
- var enemies = get_tree().get_nodes_in_group("enemies")
1986
- var nearest: CharacterBody3D = null
1987
- var min_dist = 9999.0
1988
- for e in enemies:
1989
- if is_instance_valid(e):
1990
- var d = global_position.distance_to(e.global_position)
1991
- if d < min_dist:
1992
- min_dist = d
1993
- nearest = e
1994
- return nearest
1995
-
1996
- func _process_magnet():
1997
- for area in magnet_area.get_overlapping_areas():
1998
- if area.is_in_group("gems") and area.has_method("attract_to"):
1999
- area.attract_to(self)
2000
-
2001
- func _update_magnet_shape():
2002
- var shape = magnet_area.get_node("CollisionShape3D").shape as SphereShape3D
2003
- if shape:
2004
- shape.radius = magnet_radius
2005
-
2006
- func take_damage(amount: int):
2007
- if invulnerable_timer > 0:
2008
- return
2009
- invulnerable_timer = 0.35
2010
- current_health -= amount
2011
- SoundManager.play_hurt()
2012
- emit_signal("health_changed", current_health, max_health)
2013
-
2014
- if current_health <= 0:
2015
- current_health = 0
2016
- _reset_touch()
2017
- emit_signal("died")
2018
-
2019
- func heal(amount: int):
2020
- current_health = min(max_health, current_health + amount)
2021
- emit_signal("health_changed", current_health, max_health)
2022
-
2023
- func add_xp(amount: int):
2024
- current_xp += amount
2025
- if current_xp >= required_xp:
2026
- current_xp -= required_xp
2027
- level += 1
2028
- required_xp = int(required_xp * 1.4)
2029
- SoundManager.play_level_up()
2030
- _reset_touch()
2031
- emit_signal("leveled_up", level)
2032
- emit_signal("xp_changed", current_xp, required_xp, level)
2033
-
2034
- func apply_upgrade(upgrade_id: String):
2035
- match upgrade_id:
2036
- "extra_orb":
2037
- orbit_count += 1
2038
- orbit_shield.set_orb_count(orbit_count)
2039
- "fire_rate":
2040
- projectile_cooldown = max(0.2, projectile_cooldown * 0.75)
2041
- "multi_shot":
2042
- projectile_count += 1
2043
- "proj_dmg":
2044
- projectile_damage = int(projectile_damage * 1.35)
2045
- "lightning":
2046
- if not lightning_unlocked:
2047
- lightning_unlocked = true
2048
- else:
2049
- lightning_damage = int(lightning_damage * 1.4)
2050
- lightning_cooldown = max(0.9, lightning_cooldown * 0.85)
2051
- "move_speed":
2052
- move_speed += 1.2
2053
- "max_hp":
2054
- max_health += 25
2055
- current_health += 25
2056
- emit_signal("health_changed", current_health, max_health)
2057
- "magnet":
2058
- magnet_radius += 2.5
2059
- _update_magnet_shape()
2060
- `,
2061
- );
2062
-
2063
- // scenes/player.tscn
2064
- writeFile(
2065
- "scenes/player.tscn",
2066
- `
2067
- [gd_scene load_steps=6 format=3 uid="uid://player0001"]
2068
-
2069
- [ext_resource type="Script" path="res://scripts/player.gd" id="1_play"]
2070
- [ext_resource type="PackedScene" path="res://assets/player.scad" id="2_pmesh"]
2071
- [ext_resource type="Script" path="res://scripts/orbit_shield.gd" id="3_shield"]
2072
-
2073
- [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
2074
- radius = 0.6
2075
- height = 2.4
2076
-
2077
- [sub_resource type="SphereShape3D" id="SphereShape3D_mag"]
2078
- radius = 4.5
2079
-
2080
- [node name="Player" type="CharacterBody3D"]
2081
- collision_layer = 2
2082
- collision_mask = 5
2083
- axis_lock_linear_y = true
2084
- motion_mode = 1
2085
- script = ExtResource("1_play")
2086
-
2087
- [node name="Model" parent="." instance=ExtResource("2_pmesh")]
2088
- transform = Transform3D(0.7, 0, 0, 0, 0.7, 0, 0, 0, 0.7, 0, 0, 0)
2089
-
2090
- [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
2091
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2, 0)
2092
- shape = SubResource("CapsuleShape3D_1")
2093
-
2094
- [node name="MagnetArea" type="Area3D" parent="."]
2095
- collision_layer = 0
2096
- collision_mask = 8
2097
-
2098
- [node name="CollisionShape3D" type="CollisionShape3D" parent="MagnetArea"]
2099
- shape = SubResource("SphereShape3D_mag")
2100
-
2101
- [node name="OrbitShield" type="Node3D" parent="."]
2102
- script = ExtResource("3_shield")
2103
- `,
2104
- );
2105
-
2106
- // scripts/ui.gd
2107
- writeFile(
2108
- "scripts/ui.gd",
2109
- `
2110
- extends CanvasLayer
2111
-
2112
- @onready var hp_bar: ProgressBar = $HUD/HPBar
2113
- @onready var xp_bar: ProgressBar = $HUD/XPBar
2114
- @onready var level_label: Label = $HUD/LevelLabel
2115
- @onready var timer_label: Label = $HUD/TimerLabel
2116
- @onready var kill_label: Label = $HUD/KillLabel
2117
- @onready var level_up_panel: Panel = $LevelUpPanel
2118
- @onready var cards_container: HBoxContainer = $LevelUpPanel/CardsContainer
2119
- @onready var game_over_panel: Panel = $GameOverPanel
2120
- @onready var victory_panel: Panel = $VictoryPanel
2121
-
2122
- var player: CharacterBody3D
2123
-
2124
- const ALL_UPGRADES = [
2125
- {"id": "extra_orb", "name": "Holy Aegis +1", "desc": "Add an extra orbiting sacred energy orb to shred touching enemies."},
2126
- {"id": "multi_shot", "name": "Multi-Missile +1", "desc": "Fire an additional projectile in a spread."},
2127
- {"id": "fire_rate", "name": "Rapid Casting", "desc": "Increase missile fire rate by 25%."},
2128
- {"id": "proj_dmg", "name": "Sharpened Daggers", "desc": "Boost projectile damage by 35%."},
2129
- {"id": "lightning", "name": "Heaven's Thunder", "desc": "Unlock or empower lightning strikes crashing down on enemy hordes."},
2130
- {"id": "move_speed", "name": "Boots of Haste", "desc": "Increase movement speed by +1.2 units."},
2131
- {"id": "max_hp", "name": "Titan's Vitality", "desc": "Increase max health by +25 and heal immediately."},
2132
- {"id": "magnet", "name": "Astral Attraction", "desc": "Significantly enlarge your Gem magnet pickup aura."}
2133
- ]
2134
-
2135
- func setup(p: CharacterBody3D):
2136
- player = p
2137
- player.health_changed.connect(_on_health_changed)
2138
- player.xp_changed.connect(_on_xp_changed)
2139
- player.leveled_up.connect(_on_leveled_up)
2140
- player.died.connect(_on_died)
2141
- level_up_panel.visible = false
2142
- game_over_panel.visible = false
2143
- victory_panel.visible = false
2144
-
2145
- func update_timer(seconds: float):
2146
- var m = int(seconds / 60.0)
2147
- var s = int(seconds) % 60
2148
- timer_label.text = "%02d:%02d" % [m, s]
2149
-
2150
- func update_kills(kills: int):
2151
- kill_label.text = "Kills: %d" % kills
2152
-
2153
- func _on_health_changed(current: int, max_hp: int):
2154
- hp_bar.max_value = max_hp
2155
- hp_bar.value = current
2156
- hp_bar.get_node("Label").text = "HP: %d / %d" % [current, max_hp]
2157
-
2158
- func _on_xp_changed(current: int, required: int, lvl: int):
2159
- xp_bar.max_value = required
2160
- xp_bar.value = current
2161
- level_label.text = "LVL %d" % lvl
2162
-
2163
- func _on_leveled_up(_new_lvl: int):
2164
- get_tree().paused = true
2165
- level_up_panel.visible = true
2166
-
2167
- for child in cards_container.get_children():
2168
- child.queue_free()
2169
-
2170
- var pool = ALL_UPGRADES.duplicate()
2171
- pool.shuffle()
2172
- var choices = pool.slice(0, 3)
2173
-
2174
- for upg in choices:
2175
- var btn = Button.new()
2176
- btn.custom_minimum_size = Vector2(260, 320)
2177
- btn.text = "\\n\\n" + upg["name"] + "\\n\\n" + upg["desc"] + "\\n\\n\\n[ Select ]"
2178
- btn.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
2179
- btn.pressed.connect(func(): _choose_upgrade(upg["id"]))
2180
- cards_container.add_child(btn)
2181
-
2182
- func _choose_upgrade(upgrade_id: String):
2183
- player.apply_upgrade(upgrade_id)
2184
- level_up_panel.visible = false
2185
- get_tree().paused = false
2186
-
2187
- func _on_died():
2188
- get_tree().paused = true
2189
- game_over_panel.visible = true
2190
-
2191
- func show_victory():
2192
- get_tree().paused = true
2193
- victory_panel.visible = true
2194
-
2195
- func _on_restart_pressed():
2196
- get_tree().paused = false
2197
- get_tree().reload_current_scene()
2198
- `,
2199
- );
2200
-
2201
- // scenes/ui.tscn
2202
- // HUD controls set to mouse_filter = 2 (IGNORE) so touch events flow directly to player input
2203
- writeFile(
2204
- "scenes/ui.tscn",
2205
- `
2206
- [gd_scene load_steps=2 format=3 uid="uid://ui00000001"]
2207
-
2208
- [ext_resource type="Script" path="res://scripts/ui.gd" id="1_ui"]
2209
-
2210
- [node name="UI" type="CanvasLayer"]
2211
- process_mode = 3
2212
- script = ExtResource("1_ui")
2213
-
2214
- [node name="HUD" type="Control" parent="."]
2215
- layout_mode = 3
2216
- anchors_preset = 15
2217
- anchor_right = 1.0
2218
- anchor_bottom = 1.0
2219
- grow_horizontal = 2
2220
- grow_vertical = 2
2221
- mouse_filter = 2
2222
-
2223
- [node name="XPBar" type="ProgressBar" parent="HUD"]
2224
- layout_mode = 1
2225
- anchors_preset = 10
2226
- anchor_right = 1.0
2227
- offset_bottom = 24.0
2228
- grow_horizontal = 2
2229
- mouse_filter = 2
2230
- value = 30.0
2231
- show_percentage = false
2232
-
2233
- [node name="LevelLabel" type="Label" parent="HUD"]
2234
- layout_mode = 1
2235
- anchors_preset = 5
2236
- anchor_left = 0.5
2237
- anchor_right = 0.5
2238
- offset_left = -60.0
2239
- offset_top = 28.0
2240
- offset_right = 60.0
2241
- offset_bottom = 54.0
2242
- grow_horizontal = 2
2243
- mouse_filter = 2
2244
- theme_override_font_sizes/font_size = 22
2245
- text = "LVL 1"
2246
- horizontal_alignment = 1
2247
-
2248
- [node name="TimerLabel" type="Label" parent="HUD"]
2249
- layout_mode = 1
2250
- anchors_preset = 5
2251
- anchor_left = 0.5
2252
- anchor_right = 0.5
2253
- offset_left = -60.0
2254
- offset_top = 58.0
2255
- offset_right = 60.0
2256
- offset_bottom = 90.0
2257
- grow_horizontal = 2
2258
- mouse_filter = 2
2259
- theme_override_font_sizes/font_size = 28
2260
- text = "00:00"
2261
- horizontal_alignment = 1
2262
-
2263
- [node name="KillLabel" type="Label" parent="HUD"]
2264
- layout_mode = 1
2265
- offset_left = 24.0
2266
- offset_top = 36.0
2267
- offset_right = 180.0
2268
- offset_bottom = 68.0
2269
- mouse_filter = 2
2270
- theme_override_font_sizes/font_size = 20
2271
- text = "Kills: 0"
2272
-
2273
- [node name="HPBar" type="ProgressBar" parent="HUD"]
2274
- layout_mode = 1
2275
- anchors_preset = 7
2276
- anchor_left = 0.5
2277
- anchor_top = 1.0
2278
- anchor_right = 0.5
2279
- anchor_bottom = 1.0
2280
- offset_left = -200.0
2281
- offset_top = -60.0
2282
- offset_right = 200.0
2283
- offset_bottom = -28.0
2284
- grow_horizontal = 2
2285
- grow_vertical = 0
2286
- mouse_filter = 2
2287
- value = 100.0
2288
- show_percentage = false
2289
-
2290
- [node name="Label" type="Label" parent="HUD/HPBar"]
2291
- layout_mode = 1
2292
- anchors_preset = 15
2293
- anchor_right = 1.0
2294
- anchor_bottom = 1.0
2295
- grow_horizontal = 2
2296
- grow_vertical = 2
2297
- mouse_filter = 2
2298
- text = "HP: 100 / 100"
2299
- horizontal_alignment = 1
2300
- vertical_alignment = 1
2301
-
2302
- [node name="LevelUpPanel" type="Panel" parent="."]
2303
- visible = false
2304
- anchors_preset = 15
2305
- anchor_right = 1.0
2306
- anchor_bottom = 1.0
2307
- grow_horizontal = 2
2308
- grow_vertical = 2
2309
-
2310
- [node name="Title" type="Label" parent="LevelUpPanel"]
2311
- layout_mode = 1
2312
- anchors_preset = 5
2313
- anchor_left = 0.5
2314
- anchor_right = 0.5
2315
- offset_left = -200.0
2316
- offset_top = 60.0
2317
- offset_right = 200.0
2318
- offset_bottom = 120.0
2319
- grow_horizontal = 2
2320
- theme_override_font_sizes/font_size = 40
2321
- text = "LEVEL UP!"
2322
- horizontal_alignment = 1
2323
-
2324
- [node name="Subtitle" type="Label" parent="LevelUpPanel"]
2325
- layout_mode = 1
2326
- anchors_preset = 5
2327
- anchor_left = 0.5
2328
- anchor_right = 0.5
2329
- offset_left = -200.0
2330
- offset_top = 120.0
2331
- offset_right = 200.0
2332
- offset_bottom = 150.0
2333
- grow_horizontal = 2
2334
- text = "Choose an upgrade to empower your warrior"
2335
- horizontal_alignment = 1
2336
-
2337
- [node name="CardsContainer" type="HBoxContainer" parent="LevelUpPanel"]
2338
- layout_mode = 1
2339
- anchors_preset = 8
2340
- anchor_left = 0.5
2341
- anchor_top = 0.5
2342
- anchor_right = 0.5
2343
- anchor_bottom = 0.5
2344
- offset_left = -440.0
2345
- offset_top = -140.0
2346
- offset_right = 440.0
2347
- offset_bottom = 200.0
2348
- grow_horizontal = 2
2349
- grow_vertical = 2
2350
- theme_override_constants/separation = 30
2351
- alignment = 1
2352
-
2353
- [node name="GameOverPanel" type="Panel" parent="."]
2354
- visible = false
2355
- anchors_preset = 15
2356
- anchor_right = 1.0
2357
- anchor_bottom = 1.0
2358
- grow_horizontal = 2
2359
- grow_vertical = 2
2360
-
2361
- [node name="Title" type="Label" parent="GameOverPanel"]
2362
- layout_mode = 1
2363
- anchors_preset = 8
2364
- anchor_left = 0.5
2365
- anchor_top = 0.5
2366
- anchor_right = 0.5
2367
- anchor_bottom = 0.5
2368
- offset_left = -200.0
2369
- offset_top = -100.0
2370
- offset_right = 200.0
2371
- offset_bottom = -40.0
2372
- grow_horizontal = 2
2373
- grow_vertical = 2
2374
- theme_override_colors/font_color = Color(1, 0.2, 0.2, 1)
2375
- theme_override_font_sizes/font_size = 48
2376
- text = "YOU FELL"
2377
- horizontal_alignment = 1
2378
-
2379
- [node name="Sub" type="Label" parent="GameOverPanel"]
2380
- layout_mode = 1
2381
- anchors_preset = 8
2382
- anchor_left = 0.5
2383
- anchor_top = 0.5
2384
- anchor_right = 0.5
2385
- anchor_bottom = 0.5
2386
- offset_left = -200.0
2387
- offset_top = -30.0
2388
- offset_right = 200.0
2389
- offset_bottom = 0.0
2390
- grow_horizontal = 2
2391
- grow_vertical = 2
2392
- text = "The horde has claimed your soul."
2393
- horizontal_alignment = 1
2394
-
2395
- [node name="RestartBtn" type="Button" parent="GameOverPanel"]
2396
- layout_mode = 1
2397
- anchors_preset = 8
2398
- anchor_left = 0.5
2399
- anchor_top = 0.5
2400
- anchor_right = 0.5
2401
- anchor_bottom = 0.5
2402
- offset_left = -100.0
2403
- offset_top = 40.0
2404
- offset_right = 100.0
2405
- offset_bottom = 90.0
2406
- grow_horizontal = 2
2407
- grow_vertical = 2
2408
- theme_override_font_sizes/font_size = 20
2409
- text = "Try Again"
2410
-
2411
- [node name="VictoryPanel" type="Panel" parent="."]
2412
- visible = false
2413
- anchors_preset = 15
2414
- anchor_right = 1.0
2415
- anchor_bottom = 1.0
2416
- grow_horizontal = 2
2417
- grow_vertical = 2
2418
-
2419
- [node name="Title" type="Label" parent="VictoryPanel"]
2420
- layout_mode = 1
2421
- anchors_preset = 8
2422
- anchor_left = 0.5
2423
- anchor_top = 0.5
2424
- anchor_right = 0.5
2425
- anchor_bottom = 0.5
2426
- offset_left = -250.0
2427
- offset_top = -100.0
2428
- offset_right = 250.0
2429
- offset_bottom = -40.0
2430
- grow_horizontal = 2
2431
- grow_vertical = 2
2432
- theme_override_colors/font_color = Color(1, 0.85, 0.1, 1)
2433
- theme_override_font_sizes/font_size = 48
2434
- text = "SURVIVAL ACHIEVED!"
2435
- horizontal_alignment = 1
2436
-
2437
- [node name="Sub" type="Label" parent="VictoryPanel"]
2438
- layout_mode = 1
2439
- anchors_preset = 8
2440
- anchor_left = 0.5
2441
- anchor_top = 0.5
2442
- anchor_right = 0.5
2443
- anchor_bottom = 0.5
2444
- offset_left = -200.0
2445
- offset_top = -30.0
2446
- offset_right = 200.0
2447
- offset_bottom = 0.0
2448
- grow_horizontal = 2
2449
- grow_vertical = 2
2450
- text = "Dawn breaks. You conquered the darkness."
2451
- horizontal_alignment = 1
2452
-
2453
- [node name="RestartBtn" type="Button" parent="VictoryPanel"]
2454
- layout_mode = 1
2455
- anchors_preset = 8
2456
- anchor_left = 0.5
2457
- anchor_top = 0.5
2458
- anchor_right = 0.5
2459
- anchor_bottom = 0.5
2460
- offset_left = -100.0
2461
- offset_top = 40.0
2462
- offset_right = 100.0
2463
- offset_bottom = 90.0
2464
- grow_horizontal = 2
2465
- grow_vertical = 2
2466
- theme_override_font_sizes/font_size = 20
2467
- text = "Play Again"
2468
-
2469
- [connection signal="pressed" from="GameOverPanel/RestartBtn" to="." method="_on_restart_pressed"]
2470
- [connection signal="pressed" from="VictoryPanel/RestartBtn" to="." method="_on_restart_pressed"]
2471
- `,
2472
- );
2473
-
2474
- // scripts/main.gd
2475
- writeFile(
2476
- "scripts/main.gd",
2477
- `
2478
- extends Node3D
2479
-
2480
- @export var win_time: float = 300.0
2481
-
2482
- var game_time: float = 0.0
2483
- var total_kills: int = 0
2484
- var next_spawn_time: float = 0.0
2485
-
2486
- @onready var player: CharacterBody3D = $Player
2487
- @onready var camera: Camera3D = $Camera3D
2488
- @onready var ui: CanvasLayer = $UI
2489
-
2490
- var walker_scene = preload("res://scenes/enemy_walker.tscn")
2491
- var swarmer_scene = preload("res://scenes/enemy_swarmer.tscn")
2492
- var brute_scene = preload("res://scenes/enemy_brute.tscn")
2493
-
2494
- var boss_spawned_3min: bool = false
2495
- var boss_spawned_4min: bool = false
2496
-
2497
- func _ready():
2498
- add_to_group("main")
2499
- ui.setup(player)
2500
-
2501
- func _process(delta: float):
2502
- game_time += delta
2503
- ui.update_timer(game_time)
2504
-
2505
- if player and is_instance_valid(player):
2506
- var target_pos = player.global_position + Vector3(0, 18, 14)
2507
- camera.global_position = camera.global_position.lerp(target_pos, 8.0 * delta)
2508
- camera.look_at(player.global_position + Vector3(0, 0.5, 0), Vector3.UP)
2509
-
2510
- _process_spawning(delta)
2511
-
2512
- if game_time >= win_time:
2513
- ui.show_victory()
2514
-
2515
- func _physics_process(_delta: float):
2516
- # Fast Grid physics tracking for custom enemy separation avoiding Godot engine bottlenecks
2517
- var enemies = get_tree().get_nodes_in_group("enemies")
2518
- SpatialGrid.update_grid(enemies)
2519
-
2520
- func _process_spawning(delta: float):
2521
- next_spawn_time -= delta
2522
- var current_spawn_interval = max(0.2, 1.2 - (game_time / 180.0) * 0.9)
2523
-
2524
- if next_spawn_time <= 0.0:
2525
- next_spawn_time = current_spawn_interval
2526
- _spawn_wave()
2527
-
2528
- if game_time >= 90.0 and not boss_spawned_3min:
2529
- boss_spawned_3min = true
2530
- _spawn_brute()
2531
-
2532
- if game_time >= 210.0 and not boss_spawned_4min:
2533
- boss_spawned_4min = true
2534
- _spawn_brute()
2535
- _spawn_brute()
2536
-
2537
- func _spawn_wave():
2538
- if not player or not is_instance_valid(player):
2539
- return
2540
-
2541
- var count = 1 + int(game_time / 45.0)
2542
- for i in range(count):
2543
- var angle = randf() * TAU
2544
- var dist = randf_range(20.0, 26.0)
2545
-
2546
- var enemy = null
2547
- var r = randf()
2548
- if game_time > 40.0 and r < 0.4:
2549
- enemy = swarmer_scene.instantiate()
2550
- else:
2551
- enemy = walker_scene.instantiate()
2552
-
2553
- var spawn_pos = Vector3(player.global_position.x + cos(angle) * dist, enemy.target_height, player.global_position.z + sin(angle) * dist)
2554
- add_child(enemy)
2555
- enemy.global_position = spawn_pos
2556
-
2557
- func _spawn_brute():
2558
- if not player or not is_instance_valid(player):
2559
- return
2560
- var angle = randf() * TAU
2561
- var brute = brute_scene.instantiate()
2562
- var spawn_pos = Vector3(player.global_position.x + cos(angle) * 22.0, brute.target_height, player.global_position.z + sin(angle) * 22.0)
2563
- add_child(brute)
2564
- brute.global_position = spawn_pos
2565
-
2566
- func register_kill():
2567
- total_kills += 1
2568
- ui.update_kills(total_kills)
2569
- `,
2570
- );
2571
-
2572
- // scenes/main.tscn
2573
- writeFile(
2574
- "scenes/main.tscn",
2575
- `
2576
- [gd_scene load_steps=9 format=3 uid="uid://main0000001"]
2577
-
2578
- [ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
2579
- [ext_resource type="PackedScene" path="res://scenes/player.tscn" id="2_play"]
2580
- [ext_resource type="PackedScene" path="res://scenes/ui.tscn" id="3_ui"]
2581
-
2582
- [sub_resource type="PlaneMesh" id="PlaneMesh_arena"]
2583
- size = Vector2(300, 300)
2584
-
2585
- [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
2586
- albedo_color = Color(0.12, 0.14, 0.18, 1)
2587
- roughness = 0.85
2588
- uv1_scale = Vector3(60, 60, 60)
2589
-
2590
- [sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
2591
- size = Vector3(300, 1, 300)
2592
-
2593
- [sub_resource type="Environment" id="Environment_dark"]
2594
- background_mode = 1
2595
- background_color = Color(0.04, 0.05, 0.08, 1)
2596
- ambient_light_source = 2
2597
- ambient_light_color = Color(0.15, 0.18, 0.25, 1)
2598
- ambient_light_energy = 1.0
2599
- tonemap_mode = 3
2600
- glow_enabled = true
2601
- glow_bloom = 0.35
2602
-
2603
- [node name="Main" type="Node3D"]
2604
- script = ExtResource("1_main")
2605
-
2606
- [node name="WorldEnvironment" type="WorldEnvironment" parent="."]
2607
- environment = SubResource("Environment_dark")
2608
-
2609
- [node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
2610
- transform = Transform3D(0.866, -0.354, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 20, 30, 20)
2611
- light_color = Color(0.85, 0.9, 1, 1)
2612
- light_energy = 1.4
2613
- shadow_enabled = true
2614
-
2615
- [node name="Floor" type="StaticBody3D" parent="."]
2616
- collision_layer = 1
2617
- collision_mask = 0
2618
-
2619
- [node name="MeshInstance3D" type="MeshInstance3D" parent="Floor"]
2620
- mesh = SubResource("PlaneMesh_arena")
2621
- surface_material_override/0 = SubResource("StandardMaterial3D_floor")
2622
-
2623
- [node name="CollisionShape3D" type="CollisionShape3D" parent="Floor"]
2624
- transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0)
2625
- shape = SubResource("BoxShape3D_floor")
2626
-
2627
- [node name="Player" parent="." instance=ExtResource("2_play")]
2628
-
2629
- [node name="Camera3D" type="Camera3D" parent="."]
2630
- transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 18, 14)
2631
- current = true
2632
- fov = 55.0
2633
-
2634
- [node name="UI" parent="." instance=ExtResource("3_ui")]
2635
- `,
2636
- );
2637
-
2638
- // =========================================================================
2639
- // 4. CONFIGURATION & METADATA
2640
- // =========================================================================
2641
-
2642
- writeFile(
2643
- "project.godot",
2644
- `
2645
- config_version=5
2646
-
2647
- [application]
2648
-
2649
- config/name="Horde Survivor 3D"
2650
- config/description="A high-octane 3D horde survival bullet heaven game inspired by Vampire Survivors."
2651
- run/main_scene="res://scenes/main.tscn"
2652
- config/features=PackedStringArray("4.3", "Forward Plus")
2653
-
2654
- [autoload]
2655
-
2656
- SoundManager="*res://scripts/sound_manager.gd"
2657
- SpatialGrid="*res://scripts/spatial_grid.gd"
2658
-
2659
- [editor_plugins]
2660
-
2661
- enabled=PackedStringArray("res://addons/scad_importer/plugin.cfg")
2662
-
2663
- [input_devices]
2664
-
2665
- pointing/emulate_touch_from_mouse=true
2666
- pointing/emulate_mouse_from_touch=true
2667
-
2668
- [layer_names]
2669
-
2670
- 3d_physics/layer_1="World"
2671
- 3d_physics/layer_2="Player"
2672
- 3d_physics/layer_3="Enemies"
2673
- 3d_physics/layer_4="Pickups"
2674
- 3d_physics/layer_5="Weapons"
2675
-
2676
- [input]
2677
-
2678
- move_left={
2679
- "deadzone": 0.5,
2680
- "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
2681
- , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":97,"echo":false,"script":null)
2682
- ]
2683
- }
2684
- move_right={
2685
- "deadzone": 0.5,
2686
- "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
2687
- , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":100,"echo":false,"script":null)
2688
- ]
2689
- }
2690
- move_up={
2691
- "deadzone": 0.5,
2692
- "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
2693
- , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":119,"echo":false,"script":null)
2694
- ]
2695
- }
2696
- move_down={
2697
- "deadzone": 0.5,
2698
- "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null)
2699
- , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"echo":false,"script":null)
2700
- ]
2701
- }
2702
-
2703
- [display]
2704
-
2705
- window/size/viewport_width=1280
2706
- window/size/viewport_height=720
2707
- window/stretch/mode="canvas_items"
2708
- window/stretch/aspect="expand"
2709
-
2710
- [rendering]
2711
-
2712
- anti_aliasing/quality/msaa_3d=2
2713
- glow/enabled=true
2714
- `,
2715
- );
2716
-
2717
- writeFile(
2718
- ".gitignore",
2719
- `
2720
- .godot/
2721
- *.translation
2722
- user/
2723
- `,
2724
- );
2725
-
2726
- writeFile(
2727
- "README.md",
2728
- `
2729
- # Horde Survivor 3D
2730
-
2731
- A 3D auto-shooter / horde survival game inspired by *Vampire Survivors*.
2732
-
2733
- ## Touch Screen Controls
2734
- - Touch and drag anywhere on the display to smoothly guide the hero with an invisible, dynamic virtual joystick.
2735
- - Does not clutter the screen with visible on-screen buttons or virtual pads.
2736
- - Upgrades and menu buttons respond immediately to direct taps.
2737
-
2738
- ## Physics Optimization
2739
- - Replaced the CPU-heavy Physics Engine character body repulsion with an ultra-fast **Spatial Grid Hashing** setup.
2740
- - Enemies manually separate and flock using 2D grid neighbor lookups, keeping CharacterBody3D nodes running at high frame rates.
2741
-
2742
- ## Model & Asset Polish:
2743
- - **The Brute**: Spikes project forward (+Y) out of chest armor. Features spiked pauldrons, curved horns, and a two-handed war maul with a molten glowing core.
2744
- - **The Bat (Swarmer)**: Designed with organic swept wings, elbow spars, wing claws, skeletal ribs, and flapping animations.
2745
- - **The Zombie (Walker)**: Detailed with sunken eye sockets, glowing red pupils, a decaying jaw with teeth, exposed ribs, and clawed reaching hands.
2746
- - **The Hero (Knight)**: Wields an angled runic broadsword in the right hand and an emblazoned heater kite shield on the left forearm.
2747
- `,
2748
- );
2749
-
2750
- console.log("\n======================================================");
2751
- console.log('Project "Horde Survivor 3D" updated with Touch Controls!');
2752
- console.log(`Directory: ${ROOT_DIR}`);
2753
- console.log("======================================================\n");