spawnpoint-studio 0.1.0__py3-none-any.whl
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.
- spawnpoint/__init__.py +3 -0
- spawnpoint/_data/examples/coin_hopper.json +228 -0
- spawnpoint/_data/examples/neon_arena.json +251 -0
- spawnpoint/_data/examples/pixel_arena.json +242 -0
- spawnpoint/_data/examples/pixel_hopper.json +221 -0
- spawnpoint/_data/godot/addons/spawn_point/components/aim_component.gd +63 -0
- spawnpoint/_data/godot/addons/spawn_point/components/animator_component.gd +66 -0
- spawnpoint/_data/godot/addons/spawn_point/components/chase_ai_component.gd +107 -0
- spawnpoint/_data/godot/addons/spawn_point/components/collectible_component.gd +58 -0
- spawnpoint/_data/godot/addons/spawn_point/components/gate_component.gd +64 -0
- spawnpoint/_data/godot/addons/spawn_point/components/health_component.gd +83 -0
- spawnpoint/_data/godot/addons/spawn_point/components/hitbox_component.gd +50 -0
- spawnpoint/_data/godot/addons/spawn_point/components/hurtbox_component.gd +48 -0
- spawnpoint/_data/godot/addons/spawn_point/components/juice_component.gd +219 -0
- spawnpoint/_data/godot/addons/spawn_point/components/patrol_ai_component.gd +63 -0
- spawnpoint/_data/godot/addons/spawn_point/components/platformer_movement_component.gd +204 -0
- spawnpoint/_data/godot/addons/spawn_point/components/player_input_component.gd +77 -0
- spawnpoint/_data/godot/addons/spawn_point/components/projectile_component.gd +64 -0
- spawnpoint/_data/godot/addons/spawn_point/components/spawner_component.gd +58 -0
- spawnpoint/_data/godot/addons/spawn_point/components/stomp_component.gd +56 -0
- spawnpoint/_data/godot/addons/spawn_point/components/top_down_movement_component.gd +101 -0
- spawnpoint/_data/godot/addons/spawn_point/components/trigger_zone_component.gd +32 -0
- spawnpoint/_data/godot/addons/spawn_point/components/weapon_component.gd +83 -0
- spawnpoint/_data/godot/addons/spawn_point/manifest.json +1509 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/camera_rig.gd +71 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/cockpit.gd +62 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/entity_core.gd +98 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/fx.gd +112 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/game_state.gd +111 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/hud.gd +155 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/level.gd +19 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/sfx.gd +58 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/sp.gd +121 -0
- spawnpoint/_data/godot/addons/spawn_point/runtime/world.gd +130 -0
- spawnpoint/_data/godot/addons/spawn_point/shaders/flash.gdshader +9 -0
- spawnpoint/_data/godot/addons/spawn_point/shaders/floor_grid.gdshader +13 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/build.gd +612 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/build.tscn +6 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/capture.gd +42 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/capture.tscn +6 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/dump_manifest.gd +91 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/dump_manifest.tscn +6 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/playtest.gd +428 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/playtest.tscn +6 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/playtest_nav.gd +152 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/record.gd +89 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/record.tscn +6 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/smoke.gd +72 -0
- spawnpoint/_data/godot/addons/spawn_point/tools/smoke.tscn +6 -0
- spawnpoint/analyze.py +387 -0
- spawnpoint/build.py +146 -0
- spawnpoint/cli.py +290 -0
- spawnpoint/cockpit/__init__.py +0 -0
- spawnpoint/cockpit/server.py +473 -0
- spawnpoint/cockpit/static/app.css +111 -0
- spawnpoint/cockpit/static/app.js +327 -0
- spawnpoint/cockpit/static/index.html +79 -0
- spawnpoint/diagnostics.py +91 -0
- spawnpoint/expr.py +219 -0
- spawnpoint/gif.py +58 -0
- spawnpoint/godot.py +107 -0
- spawnpoint/graph.py +175 -0
- spawnpoint/level.py +157 -0
- spawnpoint/llm_server.py +77 -0
- spawnpoint/manifest.py +222 -0
- spawnpoint/playtest/__init__.py +104 -0
- spawnpoint/playtest/report.py +148 -0
- spawnpoint/playtest/runner.py +51 -0
- spawnpoint/playtest/tuner.py +151 -0
- spawnpoint/project.py +94 -0
- spawnpoint/py.typed +0 -0
- spawnpoint/rules.py +310 -0
- spawnpoint/sfx.py +352 -0
- spawnpoint/sprites/__init__.py +0 -0
- spawnpoint/sprites/ai.py +222 -0
- spawnpoint/sprites/contour.py +147 -0
- spawnpoint/sprites/generate.py +507 -0
- spawnpoint/sprites/importer.py +212 -0
- spawnpoint/sprites/resolve.py +139 -0
- spawnpoint/sprites/spriteset.py +159 -0
- spawnpoint/sprites/tiles.py +102 -0
- spawnpoint/studio/__init__.py +0 -0
- spawnpoint/studio/assemble.py +528 -0
- spawnpoint/studio/brief.py +197 -0
- spawnpoint/studio/creative.py +124 -0
- spawnpoint/studio/dataset.py +136 -0
- spawnpoint/studio/edits.py +206 -0
- spawnpoint/studio/levelgen.py +352 -0
- spawnpoint/studio/llm.py +145 -0
- spawnpoint/studio/orchestrator.py +341 -0
- spawnpoint/studio/prompts.py +122 -0
- spawnpoint/studio/web.py +123 -0
- spawnpoint/studio/webcheck.py +210 -0
- spawnpoint/studio_cli.py +232 -0
- spawnpoint_studio-0.1.0.dist-info/METADATA +360 -0
- spawnpoint_studio-0.1.0.dist-info/RECORD +118 -0
- spawnpoint_studio-0.1.0.dist-info/WHEEL +5 -0
- spawnpoint_studio-0.1.0.dist-info/entry_points.txt +2 -0
- spawnpoint_studio-0.1.0.dist-info/licenses/LICENSE +21 -0
- spawnpoint_studio-0.1.0.dist-info/top_level.txt +2 -0
- tests/conftest.py +49 -0
- tests/test_ai_sprites.py +122 -0
- tests/test_analyze.py +116 -0
- tests/test_cockpit.py +148 -0
- tests/test_e2e.py +33 -0
- tests/test_expr.py +59 -0
- tests/test_gif.py +19 -0
- tests/test_godot_library.py +17 -0
- tests/test_graph.py +44 -0
- tests/test_level.py +64 -0
- tests/test_live_llm.py +38 -0
- tests/test_manifest.py +54 -0
- tests/test_playtest.py +129 -0
- tests/test_rules.py +85 -0
- tests/test_sfx.py +30 -0
- tests/test_sprites.py +261 -0
- tests/test_studio.py +335 -0
- tests/test_web.py +41 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spawnpoint": 1,
|
|
3
|
+
"name": "Pixel Arena",
|
|
4
|
+
"genre": "topdown",
|
|
5
|
+
"settings": {
|
|
6
|
+
"tile_size": 32,
|
|
7
|
+
"lives": 3,
|
|
8
|
+
"seed": 21,
|
|
9
|
+
"zoom": 1.5
|
|
10
|
+
},
|
|
11
|
+
"entities": {
|
|
12
|
+
"Player": {
|
|
13
|
+
"components": {
|
|
14
|
+
"TopDownMovement": {
|
|
15
|
+
"max_speed": 250,
|
|
16
|
+
"dash_speed": 720
|
|
17
|
+
},
|
|
18
|
+
"PlayerInput": {},
|
|
19
|
+
"Aim": {
|
|
20
|
+
"mode": "mouse"
|
|
21
|
+
},
|
|
22
|
+
"Weapon": {
|
|
23
|
+
"projectile": "Bullet",
|
|
24
|
+
"fire_rate": 9,
|
|
25
|
+
"spread_deg": 5
|
|
26
|
+
},
|
|
27
|
+
"Health": {
|
|
28
|
+
"max_health": 5,
|
|
29
|
+
"invincibility_time": 0.8
|
|
30
|
+
},
|
|
31
|
+
"Hurtbox": {}
|
|
32
|
+
},
|
|
33
|
+
"sprite": {
|
|
34
|
+
"generate": {
|
|
35
|
+
"kind": "hero",
|
|
36
|
+
"color": "#29adff",
|
|
37
|
+
"features": [
|
|
38
|
+
"helmet"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"Bullet": {
|
|
44
|
+
"body": "area",
|
|
45
|
+
"components": {
|
|
46
|
+
"Projectile": {
|
|
47
|
+
"speed": 760,
|
|
48
|
+
"lifetime": 1.0
|
|
49
|
+
},
|
|
50
|
+
"Hitbox": {
|
|
51
|
+
"damage": 1,
|
|
52
|
+
"knockback": 160
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"sprite": {
|
|
56
|
+
"generate": {
|
|
57
|
+
"kind": "bullet",
|
|
58
|
+
"color": "#fff1e8"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"Chaser": {
|
|
63
|
+
"components": {
|
|
64
|
+
"TopDownMovement": {
|
|
65
|
+
"max_speed": 130
|
|
66
|
+
},
|
|
67
|
+
"ChaseAI": {
|
|
68
|
+
"detect_radius": 380
|
|
69
|
+
},
|
|
70
|
+
"Health": {
|
|
71
|
+
"max_health": 2,
|
|
72
|
+
"invincibility_time": 0.05
|
|
73
|
+
},
|
|
74
|
+
"Hurtbox": {
|
|
75
|
+
"size_scale": 1.0
|
|
76
|
+
},
|
|
77
|
+
"Hitbox": {
|
|
78
|
+
"damage": 1,
|
|
79
|
+
"knockback": 300
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"sprite": {
|
|
83
|
+
"generate": {
|
|
84
|
+
"kind": "bug",
|
|
85
|
+
"color": "#ff004d"
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"Shooter": {
|
|
90
|
+
"team": "enemy",
|
|
91
|
+
"components": {
|
|
92
|
+
"TopDownMovement": {
|
|
93
|
+
"max_speed": 90
|
|
94
|
+
},
|
|
95
|
+
"ChaseAI": {
|
|
96
|
+
"detect_radius": 420,
|
|
97
|
+
"keep_distance": 220,
|
|
98
|
+
"fire_range": 380
|
|
99
|
+
},
|
|
100
|
+
"Weapon": {
|
|
101
|
+
"projectile": "EnemyBullet",
|
|
102
|
+
"fire_rate": 1.3,
|
|
103
|
+
"spread_deg": 8,
|
|
104
|
+
"muzzle_distance": 22
|
|
105
|
+
},
|
|
106
|
+
"Health": {
|
|
107
|
+
"max_health": 3,
|
|
108
|
+
"invincibility_time": 0.05
|
|
109
|
+
},
|
|
110
|
+
"Hurtbox": {
|
|
111
|
+
"size_scale": 1.0
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
"sprite": {
|
|
115
|
+
"generate": {
|
|
116
|
+
"kind": "robot",
|
|
117
|
+
"color": "#ff77a8"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
"EnemyBullet": {
|
|
122
|
+
"body": "area",
|
|
123
|
+
"team": "enemy",
|
|
124
|
+
"components": {
|
|
125
|
+
"Projectile": {
|
|
126
|
+
"speed": 300,
|
|
127
|
+
"lifetime": 3.0
|
|
128
|
+
},
|
|
129
|
+
"Hitbox": {
|
|
130
|
+
"damage": 1,
|
|
131
|
+
"knockback": 200
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"sprite": {
|
|
135
|
+
"generate": {
|
|
136
|
+
"kind": "bullet",
|
|
137
|
+
"color": "#ff77a8"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"Portal": {
|
|
142
|
+
"body": "area",
|
|
143
|
+
"components": {
|
|
144
|
+
"Spawner": {
|
|
145
|
+
"entity": "Chaser",
|
|
146
|
+
"interval": 4.0,
|
|
147
|
+
"start_delay": 3.0,
|
|
148
|
+
"max_alive": 3,
|
|
149
|
+
"total": 6
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"juice": "none",
|
|
153
|
+
"sprite": {
|
|
154
|
+
"generate": {
|
|
155
|
+
"kind": "portal",
|
|
156
|
+
"color": "#83769c"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
"HealthPack": {
|
|
161
|
+
"body": "area",
|
|
162
|
+
"components": {
|
|
163
|
+
"Collectible": {
|
|
164
|
+
"kind": "heart",
|
|
165
|
+
"score": 0,
|
|
166
|
+
"heal": 2
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
"sprite": {
|
|
170
|
+
"generate": {
|
|
171
|
+
"kind": "heart",
|
|
172
|
+
"color": "#00e436"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"level": {
|
|
178
|
+
"map": [
|
|
179
|
+
"########################################",
|
|
180
|
+
"#......................................#",
|
|
181
|
+
"#..O...................................#",
|
|
182
|
+
"#...................X..................#",
|
|
183
|
+
"#....Z............####............Z....#",
|
|
184
|
+
"#......................................#",
|
|
185
|
+
"#.......##....................##.......#",
|
|
186
|
+
"#.......##....................##.......#",
|
|
187
|
+
"#...................+..................#",
|
|
188
|
+
"#......................................#",
|
|
189
|
+
"#......................................#",
|
|
190
|
+
"#...#..............................#...#",
|
|
191
|
+
"#...#.......X.......P.......X......#...#",
|
|
192
|
+
"#...#..............................#...#",
|
|
193
|
+
"#......................................#",
|
|
194
|
+
"#......................................#",
|
|
195
|
+
"#.......##..........+.........##.......#",
|
|
196
|
+
"#.......##....................##.......#",
|
|
197
|
+
"#......................................#",
|
|
198
|
+
"#....Z............####............Z....#",
|
|
199
|
+
"#......................................#",
|
|
200
|
+
"#...................................O..#",
|
|
201
|
+
"#......................................#",
|
|
202
|
+
"########################################"
|
|
203
|
+
],
|
|
204
|
+
"legend": {
|
|
205
|
+
"#": "solid",
|
|
206
|
+
"P": "Player",
|
|
207
|
+
"Z": "Chaser",
|
|
208
|
+
"X": "Shooter",
|
|
209
|
+
"O": "Portal",
|
|
210
|
+
"+": "HealthPack"
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"rules": [
|
|
214
|
+
{
|
|
215
|
+
"on": "Game.ready",
|
|
216
|
+
"do": [
|
|
217
|
+
"message('Score 200 to clear the arena!')"
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"on": "Chaser.died",
|
|
222
|
+
"do": [
|
|
223
|
+
"add_score(10)"
|
|
224
|
+
]
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"on": "Shooter.died",
|
|
228
|
+
"do": [
|
|
229
|
+
"add_score(25)",
|
|
230
|
+
"shake(0.3)"
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"on": "GameState.score_changed",
|
|
235
|
+
"if": "score >= 200",
|
|
236
|
+
"do": [
|
|
237
|
+
"message('Arena cleared!')",
|
|
238
|
+
"win()"
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spawnpoint": 1,
|
|
3
|
+
"name": "Pixel Hopper",
|
|
4
|
+
"genre": "platformer",
|
|
5
|
+
"settings": {
|
|
6
|
+
"tile_size": 32,
|
|
7
|
+
"lives": 3,
|
|
8
|
+
"seed": 7,
|
|
9
|
+
"zoom": 1.5
|
|
10
|
+
},
|
|
11
|
+
"entities": {
|
|
12
|
+
"Player": {
|
|
13
|
+
"components": {
|
|
14
|
+
"PlatformerMovement": {
|
|
15
|
+
"air_jumps": 1,
|
|
16
|
+
"dash_speed": 620
|
|
17
|
+
},
|
|
18
|
+
"PlayerInput": {},
|
|
19
|
+
"Health": {
|
|
20
|
+
"max_health": 3
|
|
21
|
+
},
|
|
22
|
+
"Hurtbox": {},
|
|
23
|
+
"Stomp": {}
|
|
24
|
+
},
|
|
25
|
+
"sprite": {
|
|
26
|
+
"generate": {
|
|
27
|
+
"kind": "hero",
|
|
28
|
+
"color": "#29adff",
|
|
29
|
+
"features": [
|
|
30
|
+
"mask"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"Slime": {
|
|
36
|
+
"components": {
|
|
37
|
+
"PlatformerMovement": {
|
|
38
|
+
"max_speed": 60
|
|
39
|
+
},
|
|
40
|
+
"PatrolAI": {},
|
|
41
|
+
"Health": {
|
|
42
|
+
"max_health": 1
|
|
43
|
+
},
|
|
44
|
+
"Hurtbox": {
|
|
45
|
+
"size_scale": 1.0
|
|
46
|
+
},
|
|
47
|
+
"Hitbox": {
|
|
48
|
+
"damage": 1
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"sprite": {
|
|
52
|
+
"generate": {
|
|
53
|
+
"kind": "slime",
|
|
54
|
+
"color": "#ff004d"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"Frog": {
|
|
59
|
+
"team": "enemy",
|
|
60
|
+
"components": {
|
|
61
|
+
"PlatformerMovement": {
|
|
62
|
+
"max_speed": 90,
|
|
63
|
+
"jump_velocity": 480
|
|
64
|
+
},
|
|
65
|
+
"PatrolAI": {
|
|
66
|
+
"jump_interval": 1.4
|
|
67
|
+
},
|
|
68
|
+
"Health": {
|
|
69
|
+
"max_health": 2
|
|
70
|
+
},
|
|
71
|
+
"Hurtbox": {
|
|
72
|
+
"size_scale": 1.0
|
|
73
|
+
},
|
|
74
|
+
"Hitbox": {
|
|
75
|
+
"damage": 1
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"sprite": {
|
|
79
|
+
"generate": {
|
|
80
|
+
"kind": "hopper",
|
|
81
|
+
"color": "#00e436"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"Spikes": {
|
|
86
|
+
"body": "area",
|
|
87
|
+
"team": "enemy",
|
|
88
|
+
"components": {
|
|
89
|
+
"Hitbox": {
|
|
90
|
+
"damage": 1,
|
|
91
|
+
"knockback": 420
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"juice": "none",
|
|
95
|
+
"sprite": {
|
|
96
|
+
"generate": {
|
|
97
|
+
"kind": "spikes",
|
|
98
|
+
"color": "#c2c3c7"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"Coin": {
|
|
103
|
+
"body": "area",
|
|
104
|
+
"components": {
|
|
105
|
+
"Collectible": {
|
|
106
|
+
"score": 1
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"sprite": {
|
|
110
|
+
"generate": {
|
|
111
|
+
"kind": "coin",
|
|
112
|
+
"color": "#ffec27"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"Heart": {
|
|
117
|
+
"body": "area",
|
|
118
|
+
"components": {
|
|
119
|
+
"Collectible": {
|
|
120
|
+
"kind": "heart",
|
|
121
|
+
"score": 0,
|
|
122
|
+
"heal": 1
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"sprite": {
|
|
126
|
+
"generate": {
|
|
127
|
+
"kind": "heart",
|
|
128
|
+
"color": "#ff77a8"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"Door": {
|
|
133
|
+
"body": "static",
|
|
134
|
+
"components": {
|
|
135
|
+
"Gate": {}
|
|
136
|
+
},
|
|
137
|
+
"sprite": {
|
|
138
|
+
"generate": {
|
|
139
|
+
"kind": "door",
|
|
140
|
+
"color": "#ab5236"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"Exit": {
|
|
145
|
+
"body": "area",
|
|
146
|
+
"components": {
|
|
147
|
+
"TriggerZone": {}
|
|
148
|
+
},
|
|
149
|
+
"sprite": {
|
|
150
|
+
"generate": {
|
|
151
|
+
"kind": "exit",
|
|
152
|
+
"color": "#00e436"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"level": {
|
|
158
|
+
"map": [
|
|
159
|
+
"....................................................",
|
|
160
|
+
"....................................................",
|
|
161
|
+
"....................................................",
|
|
162
|
+
"....................................................",
|
|
163
|
+
"............................................C.......",
|
|
164
|
+
"............CC.............................===......",
|
|
165
|
+
"...........====.....................................",
|
|
166
|
+
".............................CC........CC...........",
|
|
167
|
+
".......CC...............C...====......====..........",
|
|
168
|
+
"......====.........C................................",
|
|
169
|
+
".......................###..........................",
|
|
170
|
+
"..P............S.......###.S...^^...CS....F.H...D.E.",
|
|
171
|
+
"##################...############..#################",
|
|
172
|
+
"##################...############..#################"
|
|
173
|
+
],
|
|
174
|
+
"legend": {
|
|
175
|
+
"#": "solid",
|
|
176
|
+
"=": "oneway",
|
|
177
|
+
"P": "Player",
|
|
178
|
+
"S": "Slime",
|
|
179
|
+
"F": "Frog",
|
|
180
|
+
"^": "Spikes",
|
|
181
|
+
"C": "Coin",
|
|
182
|
+
"H": "Heart",
|
|
183
|
+
"D": "Door",
|
|
184
|
+
"E": "Exit"
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
"rules": [
|
|
188
|
+
{
|
|
189
|
+
"on": "Game.ready",
|
|
190
|
+
"do": [
|
|
191
|
+
"message('Collect every coin to open the door!')"
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"on": "Coin.collected",
|
|
196
|
+
"if": "count('Coin') == 0",
|
|
197
|
+
"do": [
|
|
198
|
+
"Door.open()",
|
|
199
|
+
"message('The door is open!')"
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"on": "Slime.died",
|
|
204
|
+
"do": [
|
|
205
|
+
"add_score(5)"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"on": "Frog.died",
|
|
210
|
+
"do": [
|
|
211
|
+
"add_score(10)"
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"on": "Exit.triggered",
|
|
216
|
+
"do": [
|
|
217
|
+
"win()"
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
class_name AimComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Points the entity's Weapon (and visual) at the mouse, the nearest enemy, or the movement
|
|
4
|
+
## direction.
|
|
5
|
+
|
|
6
|
+
const SP_NAME := "Aim"
|
|
7
|
+
const SP_HOSTS: Array[String] = []
|
|
8
|
+
const SP_REQUIRES: Array[String] = ["Weapon"]
|
|
9
|
+
const SP_INTERNAL: Array[String] = ["aim_direction"]
|
|
10
|
+
const SP_CATEGORY := "controller"
|
|
11
|
+
|
|
12
|
+
## "mouse", "nearest_enemy" or "movement".
|
|
13
|
+
@export_enum("mouse", "nearest_enemy", "movement") var mode: String = "mouse"
|
|
14
|
+
## Rotate the visual to face the aim direction (top-down games).
|
|
15
|
+
@export var rotate_visual := true
|
|
16
|
+
## Max distance for nearest_enemy targeting (px).
|
|
17
|
+
@export_range(0.0, 5000.0, 1.0) var max_range: float = 600.0
|
|
18
|
+
|
|
19
|
+
var _entity: Node2D
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
func _ready() -> void:
|
|
23
|
+
_entity = SP.entity_of(get_parent()) as Node2D
|
|
24
|
+
process_physics_priority = -5
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
func _physics_process(_delta: float) -> void:
|
|
28
|
+
var dir := aim_direction()
|
|
29
|
+
var weapon := SP.component(_entity, &"Weapon") as WeaponComponent
|
|
30
|
+
if weapon:
|
|
31
|
+
weapon.aim_direction = dir
|
|
32
|
+
if rotate_visual and dir != Vector2.ZERO:
|
|
33
|
+
SP.face(_entity, dir, true)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## Current aim direction (normalized, or zero if there is nothing to aim at).
|
|
37
|
+
func aim_direction() -> Vector2:
|
|
38
|
+
match mode:
|
|
39
|
+
"mouse":
|
|
40
|
+
var vp := _entity.get_viewport()
|
|
41
|
+
if vp:
|
|
42
|
+
return (_entity.get_global_mouse_position() - _entity.global_position).normalized()
|
|
43
|
+
"nearest_enemy":
|
|
44
|
+
var best: Node2D = null
|
|
45
|
+
var best_d := max_range
|
|
46
|
+
var my_team := SP.team_of(_entity)
|
|
47
|
+
for e in _entity.get_tree().get_nodes_in_group(SP.ENTITY_GROUP):
|
|
48
|
+
var n := e as Node2D
|
|
49
|
+
if n == null or n == _entity or SP.team_of(n) == my_team or SP.team_of(n) == SP.TEAM_NEUTRAL:
|
|
50
|
+
continue
|
|
51
|
+
if SP.component(n, &"Hurtbox") == null:
|
|
52
|
+
continue
|
|
53
|
+
var d := n.global_position.distance_to(_entity.global_position)
|
|
54
|
+
if d < best_d:
|
|
55
|
+
best_d = d
|
|
56
|
+
best = n
|
|
57
|
+
if best:
|
|
58
|
+
return (best.global_position - _entity.global_position).normalized()
|
|
59
|
+
"movement":
|
|
60
|
+
var mv := SP.movement(_entity)
|
|
61
|
+
if mv is TopDownMovementComponent:
|
|
62
|
+
return (mv as TopDownMovementComponent).facing_dir
|
|
63
|
+
return Vector2.ZERO
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
class_name AnimatorComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Plays the entity sprite's animations from what the entity is doing: idle, run, jump, fall and
|
|
4
|
+
## hurt. Run speed follows movement speed. Attached automatically to entities that have a sprite.
|
|
5
|
+
|
|
6
|
+
const SP_NAME := "Animator"
|
|
7
|
+
const SP_HOSTS: Array[String] = []
|
|
8
|
+
const SP_REQUIRES: Array[String] = []
|
|
9
|
+
const SP_CATEGORY := "juice"
|
|
10
|
+
|
|
11
|
+
## Seconds the hurt animation is shown after taking damage.
|
|
12
|
+
@export_range(0.0, 2.0, 0.01) var hurt_time: float = 0.25
|
|
13
|
+
## Speed (px/s) above which the entity counts as moving.
|
|
14
|
+
@export_range(0.0, 200.0, 1.0) var move_threshold: float = 15.0
|
|
15
|
+
|
|
16
|
+
## Animation currently requested (may fall back to "idle" if the sprite lacks it).
|
|
17
|
+
var state := "idle"
|
|
18
|
+
|
|
19
|
+
var _entity: Node2D
|
|
20
|
+
var _sprite: AnimatedSprite2D
|
|
21
|
+
var _hurt_left := 0.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
func _ready() -> void:
|
|
25
|
+
_entity = SP.entity_of(get_parent()) as Node2D
|
|
26
|
+
_sprite = _entity.get_node_or_null(^"Visual/Shape/Sprite") as AnimatedSprite2D
|
|
27
|
+
var health := SP.component(_entity, &"Health") as HealthComponent
|
|
28
|
+
if health:
|
|
29
|
+
health.damaged.connect(func(_a: int) -> void: _hurt_left = hurt_time)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
func _process(delta: float) -> void:
|
|
33
|
+
if _sprite == null:
|
|
34
|
+
return
|
|
35
|
+
_hurt_left -= delta
|
|
36
|
+
state = pick_state()
|
|
37
|
+
var anim := state if _sprite.sprite_frames.has_animation(state) else &"idle"
|
|
38
|
+
if _sprite.animation != anim or not _sprite.is_playing():
|
|
39
|
+
_sprite.play(anim)
|
|
40
|
+
if state == "run":
|
|
41
|
+
var mv := SP.movement(_entity)
|
|
42
|
+
var top: float = mv.max_speed if mv else 200.0
|
|
43
|
+
_sprite.speed_scale = clampf(_speed() / maxf(top, 1.0) * 1.2, 0.5, 1.6)
|
|
44
|
+
else:
|
|
45
|
+
_sprite.speed_scale = 1.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## The state the entity is in right now.
|
|
49
|
+
func pick_state() -> String:
|
|
50
|
+
if _hurt_left > 0.0:
|
|
51
|
+
return "hurt"
|
|
52
|
+
var body := _entity as CharacterBody2D
|
|
53
|
+
if body == null:
|
|
54
|
+
return "idle"
|
|
55
|
+
if SP.movement(_entity) is PlatformerMovementComponent and not body.is_on_floor():
|
|
56
|
+
return "jump" if body.velocity.y < 0.0 else "fall"
|
|
57
|
+
return "run" if _speed() > move_threshold else "idle"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
func _speed() -> float:
|
|
61
|
+
var body := _entity as CharacterBody2D
|
|
62
|
+
if body == null:
|
|
63
|
+
return 0.0
|
|
64
|
+
if SP.movement(_entity) is PlatformerMovementComponent:
|
|
65
|
+
return absf(body.velocity.x)
|
|
66
|
+
return body.velocity.length()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
class_name ChaseAIComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Enemy brain: wanders until the player comes close, then chases (and shoots, if the entity
|
|
4
|
+
## has a Weapon and a clear line of sight). Works top-down; in platformers it only moves
|
|
5
|
+
## horizontally.
|
|
6
|
+
|
|
7
|
+
const SP_NAME := "ChaseAI"
|
|
8
|
+
const SP_HOSTS: Array[String] = ["CharacterBody2D"]
|
|
9
|
+
const SP_REQUIRES: Array[String] = []
|
|
10
|
+
const SP_CATEGORY := "controller"
|
|
11
|
+
|
|
12
|
+
enum State { WANDER, CHASE }
|
|
13
|
+
|
|
14
|
+
## Distance at which the player is noticed (px).
|
|
15
|
+
@export_range(0.0, 5000.0, 1.0) var detect_radius: float = 300.0
|
|
16
|
+
## Distance at which the chase is abandoned (px).
|
|
17
|
+
@export_range(0.0, 5000.0, 1.0) var lose_radius: float = 480.0
|
|
18
|
+
## Stop approaching inside this distance (ranged enemies keep their distance).
|
|
19
|
+
@export_range(0.0, 2000.0, 1.0) var keep_distance: float = 0.0
|
|
20
|
+
## Weapon fires when the player is within this distance (px).
|
|
21
|
+
@export_range(0.0, 5000.0, 1.0) var fire_range: float = 380.0
|
|
22
|
+
## Wander around while idle.
|
|
23
|
+
@export var wander := true
|
|
24
|
+
## Fraction of max speed used while wandering.
|
|
25
|
+
@export_range(0.0, 1.0, 0.05) var wander_speed: float = 0.35
|
|
26
|
+
## Require an unobstructed line to the player to chase/shoot.
|
|
27
|
+
@export var needs_line_of_sight := true
|
|
28
|
+
|
|
29
|
+
var state := State.WANDER
|
|
30
|
+
|
|
31
|
+
var _body: CharacterBody2D
|
|
32
|
+
var _wander_dir := Vector2.ZERO
|
|
33
|
+
var _wander_timer := 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
func _ready() -> void:
|
|
37
|
+
_body = get_parent() as CharacterBody2D
|
|
38
|
+
process_physics_priority = -10
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
func _physics_process(delta: float) -> void:
|
|
42
|
+
var mv := SP.movement(_body)
|
|
43
|
+
var weapon := SP.component(_body, &"Weapon") as WeaponComponent
|
|
44
|
+
var player := World.player()
|
|
45
|
+
var to_player := Vector2.ZERO
|
|
46
|
+
var dist := INF
|
|
47
|
+
if player and player.visible and GameState.is_playing():
|
|
48
|
+
to_player = player.global_position - _body.global_position
|
|
49
|
+
dist = to_player.length()
|
|
50
|
+
|
|
51
|
+
match state:
|
|
52
|
+
State.WANDER:
|
|
53
|
+
if dist < detect_radius and _can_see(player):
|
|
54
|
+
state = State.CHASE
|
|
55
|
+
State.CHASE:
|
|
56
|
+
if dist > lose_radius or player == null or not player.visible:
|
|
57
|
+
state = State.WANDER
|
|
58
|
+
|
|
59
|
+
var desired := Vector2.ZERO
|
|
60
|
+
if state == State.CHASE:
|
|
61
|
+
if dist > keep_distance:
|
|
62
|
+
desired = to_player.normalized()
|
|
63
|
+
elif keep_distance > 0.0 and dist < keep_distance * 0.7:
|
|
64
|
+
desired = -to_player.normalized()
|
|
65
|
+
elif wander:
|
|
66
|
+
_wander_timer -= delta
|
|
67
|
+
if _wander_timer <= 0.0:
|
|
68
|
+
_wander_timer = randf_range(1.0, 2.5)
|
|
69
|
+
_wander_dir = Vector2.ZERO if randf() < 0.3 else Vector2.from_angle(randf() * TAU)
|
|
70
|
+
desired = _wander_dir * wander_speed
|
|
71
|
+
desired += _separation()
|
|
72
|
+
|
|
73
|
+
if mv is PlatformerMovementComponent:
|
|
74
|
+
mv.move_vector = Vector2(signf(desired.x) if absf(desired.x) > 0.1 else 0.0, 0)
|
|
75
|
+
elif mv:
|
|
76
|
+
mv.move_vector = desired.limit_length(1.0)
|
|
77
|
+
|
|
78
|
+
if weapon:
|
|
79
|
+
var shooting := state == State.CHASE and dist <= fire_range and _can_see(player)
|
|
80
|
+
weapon.trigger_held = shooting
|
|
81
|
+
if shooting:
|
|
82
|
+
weapon.aim_direction = to_player.normalized()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
func _can_see(target: Node2D) -> bool:
|
|
86
|
+
if target == null:
|
|
87
|
+
return false
|
|
88
|
+
if not needs_line_of_sight:
|
|
89
|
+
return true
|
|
90
|
+
var q := PhysicsRayQueryParameters2D.create(_body.global_position, target.global_position, SP.LAYER_WORLD, [_body.get_rid()])
|
|
91
|
+
return _body.get_world_2d().direct_space_state.intersect_ray(q).is_empty()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Keeps a crowd of chasers from collapsing into one blob.
|
|
95
|
+
func _separation() -> Vector2:
|
|
96
|
+
var push := Vector2.ZERO
|
|
97
|
+
var half := SP.half_size(_body)
|
|
98
|
+
var radius := maxf(half.x, half.y) * 2.5
|
|
99
|
+
for other in World.instances(StringName(SP.core(_body).entity_type)):
|
|
100
|
+
var o := other as Node2D
|
|
101
|
+
if o == _body:
|
|
102
|
+
continue
|
|
103
|
+
var d := _body.global_position - o.global_position
|
|
104
|
+
var l := d.length()
|
|
105
|
+
if l > 0.01 and l < radius:
|
|
106
|
+
push += d / l * (1.0 - l / radius)
|
|
107
|
+
return push
|