scad-gltf 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -5
- package/godot/README.md +0 -3
- package/package.json +1 -3
- package/bevy/README.md +0 -46
- package/bevy/examples/LICENSE +0 -21
- package/bevy/examples/README.md +0 -58
- package/bevy/examples/package.json +0 -5
- package/bevy/examples/packman_3d.js +0 -2415
- package/godot/examples/LICENSE +0 -21
- package/godot/examples/README.md +0 -91
- package/godot/examples/fruit_fusion_3d.js +0 -1774
- package/godot/examples/horde_survivor_3d.js +0 -2753
- package/godot/examples/kids_digit_writing.js +0 -2532
- package/godot/examples/naval_battle_3d.js +0 -1691
- package/godot/examples/package.json +0 -5
- package/godot/export_presets.cfg +0 -25
|
@@ -1,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");
|