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,204 @@
|
|
|
1
|
+
class_name PlatformerMovementComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Side-view running and jumping tuned for good game feel.
|
|
4
|
+
##
|
|
5
|
+
## Includes coyote time (jump shortly after leaving a ledge), jump buffering (press jump shortly
|
|
6
|
+
## before landing), variable jump height (release early for a short hop), stronger gravity when
|
|
7
|
+
## falling, a little hang time at the apex, snappy turning and an optional dash.
|
|
8
|
+
## Reads intent (move_vector, jump requests) from a controller such as PlayerInput or PatrolAI.
|
|
9
|
+
|
|
10
|
+
const SP_NAME := "PlatformerMovement"
|
|
11
|
+
const SP_HOSTS: Array[String] = ["CharacterBody2D"]
|
|
12
|
+
const SP_REQUIRES: Array[String] = []
|
|
13
|
+
const SP_INTERNAL: Array[String] = ["step_velocity"]
|
|
14
|
+
const SP_CATEGORY := "movement"
|
|
15
|
+
|
|
16
|
+
## Emitted when a jump starts (ground, coyote or air jump).
|
|
17
|
+
signal jumped
|
|
18
|
+
## Emitted when touching the floor after being airborne. [param impact_speed] is the fall speed.
|
|
19
|
+
signal landed(impact_speed: float)
|
|
20
|
+
## Emitted when a dash starts.
|
|
21
|
+
signal dashed
|
|
22
|
+
|
|
23
|
+
## Top running speed (px/s).
|
|
24
|
+
@export_range(0.0, 2000.0, 1.0) var max_speed: float = 230.0
|
|
25
|
+
## Ground acceleration toward max speed (px/s²).
|
|
26
|
+
@export_range(0.0, 20000.0, 10.0) var acceleration: float = 2200.0
|
|
27
|
+
## Ground deceleration when no input (px/s²).
|
|
28
|
+
@export_range(0.0, 20000.0, 10.0) var deceleration: float = 2600.0
|
|
29
|
+
## Air acceleration (px/s²). Lower = less air control.
|
|
30
|
+
@export_range(0.0, 20000.0, 10.0) var air_acceleration: float = 1500.0
|
|
31
|
+
## Air deceleration when no input (px/s²).
|
|
32
|
+
@export_range(0.0, 20000.0, 10.0) var air_deceleration: float = 800.0
|
|
33
|
+
## Initial jump speed (px/s). Jump height ≈ jump_velocity² / (2·gravity).
|
|
34
|
+
@export_range(0.0, 3000.0, 1.0) var jump_velocity: float = 620.0
|
|
35
|
+
## Multiplies the project gravity for this entity.
|
|
36
|
+
@export_range(0.0, 5.0, 0.05) var gravity_scale: float = 1.0
|
|
37
|
+
## Extra gravity while falling — makes jumps feel weighty instead of floaty.
|
|
38
|
+
@export_range(1.0, 5.0, 0.05) var fall_gravity_multiplier: float = 1.7
|
|
39
|
+
## Vertical speed is multiplied by this when jump is released while rising (short hop).
|
|
40
|
+
@export_range(0.0, 1.0, 0.05) var jump_cut: float = 0.45
|
|
41
|
+
## Gravity multiplier near the top of a held jump (a little hang time).
|
|
42
|
+
@export_range(0.1, 1.0, 0.05) var apex_gravity_multiplier: float = 0.6
|
|
43
|
+
## Vertical speed below which the apex hang applies (px/s).
|
|
44
|
+
@export_range(0.0, 500.0, 1.0) var apex_threshold: float = 70.0
|
|
45
|
+
## Terminal fall speed (px/s).
|
|
46
|
+
@export_range(50.0, 5000.0, 1.0) var max_fall_speed: float = 900.0
|
|
47
|
+
## Seconds after leaving a ledge during which jumping still works.
|
|
48
|
+
@export_range(0.0, 0.5, 0.01) var coyote_time: float = 0.1
|
|
49
|
+
## Seconds a jump press is remembered before landing.
|
|
50
|
+
@export_range(0.0, 0.5, 0.01) var jump_buffer: float = 0.12
|
|
51
|
+
## Extra jumps in the air (1 = double jump).
|
|
52
|
+
@export_range(0, 5) var air_jumps: int = 0
|
|
53
|
+
## Dash speed (px/s). 0 disables dashing.
|
|
54
|
+
@export_range(0.0, 3000.0, 1.0) var dash_speed: float = 0.0
|
|
55
|
+
## Dash duration in seconds.
|
|
56
|
+
@export_range(0.02, 1.0, 0.01) var dash_duration: float = 0.14
|
|
57
|
+
## Seconds between dashes.
|
|
58
|
+
@export_range(0.0, 5.0, 0.01) var dash_cooldown: float = 0.45
|
|
59
|
+
|
|
60
|
+
## Intent: x in -1..1 is the run direction (y is ignored).
|
|
61
|
+
var move_vector := Vector2.ZERO
|
|
62
|
+
## Intent: true while the jump button is held (enables full-height jumps).
|
|
63
|
+
var jump_held := false
|
|
64
|
+
## 1 = facing right, -1 = facing left.
|
|
65
|
+
var facing := 1
|
|
66
|
+
|
|
67
|
+
var _body: CharacterBody2D
|
|
68
|
+
var _coyote_left := 0.0
|
|
69
|
+
var _buffer_left := 0.0
|
|
70
|
+
var _air_jumps_left := 0
|
|
71
|
+
var _rising_from_jump := false
|
|
72
|
+
var _dash_requested := false
|
|
73
|
+
var _dash_left := 0.0
|
|
74
|
+
var _dash_cd := 0.0
|
|
75
|
+
var _dash_dir := 1
|
|
76
|
+
var _control_lock := 0.0
|
|
77
|
+
var _was_on_floor := true
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
func _ready() -> void:
|
|
81
|
+
_body = get_parent() as CharacterBody2D
|
|
82
|
+
if _body:
|
|
83
|
+
_body.motion_mode = CharacterBody2D.MOTION_MODE_GROUNDED
|
|
84
|
+
_body.floor_snap_length = 6.0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## Asks for a jump; it happens as soon as it is allowed (buffered).
|
|
88
|
+
func request_jump() -> void:
|
|
89
|
+
_buffer_left = jump_buffer if jump_buffer > 0.0 else 0.0001
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Asks for a dash in the move / facing direction.
|
|
93
|
+
func request_dash() -> void:
|
|
94
|
+
_dash_requested = true
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
## Launches the entity upward (e.g. after stomping an enemy). Not cut by releasing jump.
|
|
98
|
+
func bounce(speed: float) -> void:
|
|
99
|
+
if _body:
|
|
100
|
+
_body.velocity.y = -absf(speed)
|
|
101
|
+
_rising_from_jump = false
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
## Pushes the entity; player control is reduced briefly so the push is felt.
|
|
105
|
+
func apply_knockback(impulse: Vector2) -> void:
|
|
106
|
+
if _body == null:
|
|
107
|
+
return
|
|
108
|
+
_body.velocity = Vector2(impulse.x, minf(impulse.y, -absf(impulse.length()) * 0.5))
|
|
109
|
+
_control_lock = 0.18
|
|
110
|
+
_dash_left = 0.0
|
|
111
|
+
_rising_from_jump = false
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
## True while dashing.
|
|
115
|
+
func is_dashing() -> bool:
|
|
116
|
+
return _dash_left > 0.0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
func _physics_process(delta: float) -> void:
|
|
120
|
+
if _body == null:
|
|
121
|
+
return
|
|
122
|
+
var v := step_velocity(_body.velocity, delta, _body.is_on_floor())
|
|
123
|
+
_body.velocity = v
|
|
124
|
+
_body.move_and_slide()
|
|
125
|
+
var on_floor := _body.is_on_floor()
|
|
126
|
+
if on_floor and not _was_on_floor:
|
|
127
|
+
landed.emit(maxf(v.y, 0.0))
|
|
128
|
+
_was_on_floor = on_floor
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## Pure movement step: returns the new velocity. Separated from physics for testing.
|
|
132
|
+
func step_velocity(v: Vector2, delta: float, on_floor: bool) -> Vector2:
|
|
133
|
+
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity", 1600.0) * gravity_scale
|
|
134
|
+
|
|
135
|
+
if on_floor:
|
|
136
|
+
_coyote_left = coyote_time
|
|
137
|
+
_air_jumps_left = air_jumps
|
|
138
|
+
else:
|
|
139
|
+
_coyote_left -= delta
|
|
140
|
+
_buffer_left -= delta
|
|
141
|
+
_dash_cd -= delta
|
|
142
|
+
_control_lock -= delta
|
|
143
|
+
|
|
144
|
+
if move_vector.x != 0.0:
|
|
145
|
+
facing = 1 if move_vector.x > 0.0 else -1
|
|
146
|
+
|
|
147
|
+
# Dash: fixed horizontal speed, no gravity.
|
|
148
|
+
if _dash_requested and dash_speed > 0.0 and _dash_cd <= 0.0 and _dash_left <= 0.0:
|
|
149
|
+
_dash_left = dash_duration
|
|
150
|
+
_dash_cd = dash_cooldown + dash_duration
|
|
151
|
+
_dash_dir = facing
|
|
152
|
+
dashed.emit()
|
|
153
|
+
_dash_requested = false
|
|
154
|
+
if _dash_left > 0.0:
|
|
155
|
+
_dash_left -= delta
|
|
156
|
+
if _dash_left <= 0.0:
|
|
157
|
+
return Vector2(_dash_dir * max_speed, 0.0)
|
|
158
|
+
return Vector2(_dash_dir * dash_speed, 0.0)
|
|
159
|
+
|
|
160
|
+
# Horizontal.
|
|
161
|
+
var target := clampf(move_vector.x, -1.0, 1.0) * max_speed
|
|
162
|
+
var accel: float
|
|
163
|
+
if on_floor:
|
|
164
|
+
accel = acceleration if absf(target) > 0.01 else deceleration
|
|
165
|
+
else:
|
|
166
|
+
accel = air_acceleration if absf(target) > 0.01 else air_deceleration
|
|
167
|
+
if absf(target) > 0.01 and signf(target) != signf(v.x) and v.x != 0.0:
|
|
168
|
+
accel *= 1.6 # Snappy turnarounds.
|
|
169
|
+
if _control_lock > 0.0:
|
|
170
|
+
accel *= 0.15
|
|
171
|
+
v.x = move_toward(v.x, target, accel * delta)
|
|
172
|
+
|
|
173
|
+
# Jump (buffered, coyote, air jumps).
|
|
174
|
+
if _buffer_left > 0.0:
|
|
175
|
+
if on_floor or _coyote_left > 0.0:
|
|
176
|
+
v.y = _start_jump()
|
|
177
|
+
elif _air_jumps_left > 0:
|
|
178
|
+
_air_jumps_left -= 1
|
|
179
|
+
v.y = _start_jump()
|
|
180
|
+
|
|
181
|
+
# Variable jump height.
|
|
182
|
+
if _rising_from_jump and not jump_held and v.y < 0.0:
|
|
183
|
+
v.y *= jump_cut
|
|
184
|
+
_rising_from_jump = false
|
|
185
|
+
if v.y >= 0.0:
|
|
186
|
+
_rising_from_jump = false
|
|
187
|
+
|
|
188
|
+
# Gravity.
|
|
189
|
+
if not on_floor or v.y < 0.0:
|
|
190
|
+
var g := gravity
|
|
191
|
+
if v.y > 0.0:
|
|
192
|
+
g *= fall_gravity_multiplier
|
|
193
|
+
elif absf(v.y) < apex_threshold and jump_held:
|
|
194
|
+
g *= apex_gravity_multiplier
|
|
195
|
+
v.y = minf(v.y + g * delta, max_fall_speed)
|
|
196
|
+
return v
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
func _start_jump() -> float:
|
|
200
|
+
_buffer_left = 0.0
|
|
201
|
+
_coyote_left = 0.0
|
|
202
|
+
_rising_from_jump = true
|
|
203
|
+
jumped.emit()
|
|
204
|
+
return -jump_velocity
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
class_name PlayerInputComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Keyboard/mouse controller for the player. Writes intent into the movement component and
|
|
4
|
+
## pulls weapon triggers. Registers its own input actions, so no project setup is needed.
|
|
5
|
+
##
|
|
6
|
+
## Default keys: move = WASD / arrows, jump = Space / Z / W / Up, dash = Shift / C / K,
|
|
7
|
+
## shoot = X / J / left mouse.
|
|
8
|
+
|
|
9
|
+
const SP_NAME := "PlayerInput"
|
|
10
|
+
const SP_HOSTS: Array[String] = ["CharacterBody2D"]
|
|
11
|
+
const SP_REQUIRES: Array[String] = []
|
|
12
|
+
const SP_INTERNAL: Array[String] = ["ensure_actions"]
|
|
13
|
+
const SP_CATEGORY := "controller"
|
|
14
|
+
|
|
15
|
+
const ACTIONS := {
|
|
16
|
+
&"move_left": [KEY_A, KEY_LEFT],
|
|
17
|
+
&"move_right": [KEY_D, KEY_RIGHT],
|
|
18
|
+
&"move_up": [KEY_W, KEY_UP],
|
|
19
|
+
&"move_down": [KEY_S, KEY_DOWN],
|
|
20
|
+
&"jump": [KEY_SPACE, KEY_Z, KEY_W, KEY_UP],
|
|
21
|
+
&"dash": [KEY_SHIFT, KEY_C, KEY_K],
|
|
22
|
+
&"shoot": [KEY_X, KEY_J, MOUSE_BUTTON_LEFT],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var _entity: Node
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
static func ensure_actions() -> void:
|
|
29
|
+
for action: StringName in ACTIONS:
|
|
30
|
+
if InputMap.has_action(action):
|
|
31
|
+
continue
|
|
32
|
+
InputMap.add_action(action, 0.2)
|
|
33
|
+
for code: int in ACTIONS[action]:
|
|
34
|
+
var ev: InputEvent
|
|
35
|
+
if action == &"shoot" and code == MOUSE_BUTTON_LEFT:
|
|
36
|
+
var mb := InputEventMouseButton.new()
|
|
37
|
+
mb.button_index = MOUSE_BUTTON_LEFT
|
|
38
|
+
ev = mb
|
|
39
|
+
else:
|
|
40
|
+
var k := InputEventKey.new()
|
|
41
|
+
k.physical_keycode = code
|
|
42
|
+
ev = k
|
|
43
|
+
InputMap.action_add_event(action, ev)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
func _ready() -> void:
|
|
47
|
+
ensure_actions()
|
|
48
|
+
_entity = get_parent()
|
|
49
|
+
# Run before movement and weapons in the same physics frame.
|
|
50
|
+
process_physics_priority = -10
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
func _physics_process(_delta: float) -> void:
|
|
54
|
+
if not GameState.is_playing():
|
|
55
|
+
_clear_intent()
|
|
56
|
+
return
|
|
57
|
+
var mv := SP.movement(_entity)
|
|
58
|
+
if mv:
|
|
59
|
+
mv.move_vector = Input.get_vector(&"move_left", &"move_right", &"move_up", &"move_down")
|
|
60
|
+
if Input.is_action_just_pressed(&"dash"):
|
|
61
|
+
mv.request_dash()
|
|
62
|
+
if mv is PlatformerMovementComponent:
|
|
63
|
+
mv.jump_held = Input.is_action_pressed(&"jump")
|
|
64
|
+
if Input.is_action_just_pressed(&"jump"):
|
|
65
|
+
mv.request_jump()
|
|
66
|
+
var weapon := SP.component(_entity, &"Weapon") as WeaponComponent
|
|
67
|
+
if weapon:
|
|
68
|
+
weapon.trigger_held = Input.is_action_pressed(&"shoot")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
func _clear_intent() -> void:
|
|
72
|
+
var mv := SP.movement(_entity)
|
|
73
|
+
if mv:
|
|
74
|
+
mv.move_vector = Vector2.ZERO
|
|
75
|
+
var weapon := SP.component(_entity, &"Weapon") as WeaponComponent
|
|
76
|
+
if weapon:
|
|
77
|
+
weapon.trigger_held = false
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
class_name ProjectileComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Moves its entity in a straight line; disappears on walls, after its lifetime, or after
|
|
4
|
+
## hitting [member pierce] + 1 targets. Pair with a Hitbox to deal damage.
|
|
5
|
+
|
|
6
|
+
const SP_NAME := "Projectile"
|
|
7
|
+
const SP_HOSTS: Array[String] = ["Area2D"]
|
|
8
|
+
const SP_REQUIRES: Array[String] = []
|
|
9
|
+
const SP_CATEGORY := "core"
|
|
10
|
+
|
|
11
|
+
## Emitted when the projectile disappears (wall, lifetime or last hit).
|
|
12
|
+
signal expired
|
|
13
|
+
|
|
14
|
+
## Speed in px/s.
|
|
15
|
+
@export_range(0.0, 5000.0, 1.0) var speed: float = 620.0
|
|
16
|
+
## Seconds before disappearing.
|
|
17
|
+
@export_range(0.05, 20.0, 0.05) var lifetime: float = 1.4
|
|
18
|
+
## Number of extra targets it passes through.
|
|
19
|
+
@export_range(0, 99) var pierce: int = 0
|
|
20
|
+
|
|
21
|
+
## Flight direction (set by the Weapon at spawn).
|
|
22
|
+
var direction := Vector2.RIGHT
|
|
23
|
+
|
|
24
|
+
var _entity: Area2D
|
|
25
|
+
var _age := 0.0
|
|
26
|
+
var _done := false
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
func _ready() -> void:
|
|
30
|
+
_entity = get_parent() as Area2D
|
|
31
|
+
if _entity.has_meta(&"sp_direction"):
|
|
32
|
+
direction = (_entity.get_meta(&"sp_direction") as Vector2).normalized()
|
|
33
|
+
_entity.body_entered.connect(_on_body_entered)
|
|
34
|
+
var hitbox := SP.component(_entity, &"Hitbox") as HitboxComponent
|
|
35
|
+
if hitbox:
|
|
36
|
+
hitbox.hit.connect(_on_hit)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
func _physics_process(delta: float) -> void:
|
|
40
|
+
if _done:
|
|
41
|
+
return
|
|
42
|
+
_entity.global_position += direction * speed * delta
|
|
43
|
+
_age += delta
|
|
44
|
+
if _age >= lifetime:
|
|
45
|
+
expire()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Removes the projectile now.
|
|
49
|
+
func expire() -> void:
|
|
50
|
+
if _done:
|
|
51
|
+
return
|
|
52
|
+
_done = true
|
|
53
|
+
expired.emit()
|
|
54
|
+
_entity.queue_free()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
func _on_body_entered(_body: Node) -> void:
|
|
58
|
+
expire()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
func _on_hit(_target: Node) -> void:
|
|
62
|
+
pierce -= 1
|
|
63
|
+
if pierce < 0:
|
|
64
|
+
expire()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
class_name SpawnerComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Spawns entities at its position on a timer (enemy waves, portals, hazards).
|
|
4
|
+
|
|
5
|
+
const SP_NAME := "Spawner"
|
|
6
|
+
const SP_HOSTS: Array[String] = []
|
|
7
|
+
const SP_REQUIRES: Array[String] = []
|
|
8
|
+
const SP_CATEGORY := "core"
|
|
9
|
+
const SP_ENTITY_PARAMS: Array[String] = ["entity"]
|
|
10
|
+
|
|
11
|
+
## Emitted after each spawn with the new node.
|
|
12
|
+
signal spawned(node: Node)
|
|
13
|
+
|
|
14
|
+
## Entity type to spawn.
|
|
15
|
+
@export var entity: String = ""
|
|
16
|
+
## Seconds between spawns.
|
|
17
|
+
@export_range(0.05, 600.0, 0.05) var interval: float = 3.0
|
|
18
|
+
## Seconds before the first spawn.
|
|
19
|
+
@export_range(0.0, 600.0, 0.05) var start_delay: float = 1.0
|
|
20
|
+
## Do not spawn while this many spawned entities are alive.
|
|
21
|
+
@export_range(1, 500) var max_alive: int = 5
|
|
22
|
+
## Total spawns before stopping (0 = unlimited).
|
|
23
|
+
@export_range(0, 10000) var total: int = 0
|
|
24
|
+
## Random offset radius around the spawner (px).
|
|
25
|
+
@export_range(0.0, 2000.0, 1.0) var radius: float = 0.0
|
|
26
|
+
|
|
27
|
+
var _timer := 0.0
|
|
28
|
+
var _count := 0
|
|
29
|
+
var _alive: Array[Node] = []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
func _ready() -> void:
|
|
33
|
+
_timer = start_delay
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
func _physics_process(delta: float) -> void:
|
|
37
|
+
if not GameState.is_playing() or (total > 0 and _count >= total):
|
|
38
|
+
return
|
|
39
|
+
_timer -= delta
|
|
40
|
+
if _timer > 0.0:
|
|
41
|
+
return
|
|
42
|
+
_timer = interval
|
|
43
|
+
_alive.assign(_alive.filter(func(n: Node) -> bool: return is_instance_valid(n) and not n.is_queued_for_deletion()))
|
|
44
|
+
if _alive.size() >= max_alive:
|
|
45
|
+
return
|
|
46
|
+
spawn_now()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Spawns one entity immediately. Returns it (or null).
|
|
50
|
+
func spawn_now() -> Node:
|
|
51
|
+
var origin := (get_parent() as Node2D).global_position
|
|
52
|
+
var offset := Vector2.from_angle(randf() * TAU) * randf() * radius
|
|
53
|
+
var n := World.spawn(entity, origin + offset)
|
|
54
|
+
if n:
|
|
55
|
+
_count += 1
|
|
56
|
+
_alive.append(n)
|
|
57
|
+
spawned.emit(n)
|
|
58
|
+
return n
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class_name StompComponent
|
|
2
|
+
extends Area2D
|
|
3
|
+
## Platformer "jump on their head" attack: landing on an enemy from above damages it and
|
|
4
|
+
## bounces this entity up. Grants a moment of invincibility so the enemy's contact damage
|
|
5
|
+
## does not trigger in the same frame.
|
|
6
|
+
|
|
7
|
+
const SP_NAME := "Stomp"
|
|
8
|
+
const SP_HOSTS: Array[String] = ["CharacterBody2D"]
|
|
9
|
+
const SP_REQUIRES: Array[String] = ["PlatformerMovement"]
|
|
10
|
+
const SP_CATEGORY := "core"
|
|
11
|
+
|
|
12
|
+
## Emitted after a successful stomp on [param target].
|
|
13
|
+
signal stomped(target: Node)
|
|
14
|
+
|
|
15
|
+
## Damage dealt by a stomp.
|
|
16
|
+
@export_range(0, 999) var damage: int = 1
|
|
17
|
+
## Upward speed after a stomp (px/s). Holding jump does not add to it.
|
|
18
|
+
@export_range(0.0, 3000.0, 1.0) var bounce_velocity: float = 480.0
|
|
19
|
+
## Stomp area size relative to the entity shape.
|
|
20
|
+
@export_range(0.1, 2.0, 0.05) var size_scale: float = 1.0
|
|
21
|
+
|
|
22
|
+
var _body: CharacterBody2D
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
func _ready() -> void:
|
|
26
|
+
_body = SP.entity_of(get_parent()) as CharacterBody2D
|
|
27
|
+
monitorable = false
|
|
28
|
+
monitoring = true
|
|
29
|
+
collision_layer = 0
|
|
30
|
+
collision_mask = SP.opposing_hurtbox_mask(SP.team_of(_body))
|
|
31
|
+
# Run before enemy hitboxes so a stomp wins over contact damage.
|
|
32
|
+
process_physics_priority = -20
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
func _physics_process(_delta: float) -> void:
|
|
36
|
+
if _body.velocity.y <= 0.0:
|
|
37
|
+
return
|
|
38
|
+
var feet := _body.global_position.y + SP.half_size(_body).y
|
|
39
|
+
for area in get_overlapping_areas():
|
|
40
|
+
var hb := area as HurtboxComponent
|
|
41
|
+
if hb == null:
|
|
42
|
+
continue
|
|
43
|
+
var target := SP.entity_of(hb) as Node2D
|
|
44
|
+
if target == null or target == _body:
|
|
45
|
+
continue
|
|
46
|
+
# Only counts when our feet are above the target's center.
|
|
47
|
+
if feet > target.global_position.y + 2.0:
|
|
48
|
+
continue
|
|
49
|
+
var my_health := SP.component(_body, &"Health") as HealthComponent
|
|
50
|
+
if my_health:
|
|
51
|
+
my_health.grant_invincibility(0.2)
|
|
52
|
+
hb.take_hit(damage, _body.global_position, 0.0, _body)
|
|
53
|
+
var mv := SP.component(_body, &"PlatformerMovement") as PlatformerMovementComponent
|
|
54
|
+
mv.bounce(bounce_velocity)
|
|
55
|
+
stomped.emit(target)
|
|
56
|
+
return
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
class_name TopDownMovementComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## Eight-direction movement seen from above, with acceleration, friction and an optional dash.
|
|
4
|
+
## Reads intent (move_vector, dash requests) from a controller such as PlayerInput or ChaseAI.
|
|
5
|
+
|
|
6
|
+
const SP_NAME := "TopDownMovement"
|
|
7
|
+
const SP_HOSTS: Array[String] = ["CharacterBody2D"]
|
|
8
|
+
const SP_REQUIRES: Array[String] = []
|
|
9
|
+
const SP_INTERNAL: Array[String] = ["step_velocity"]
|
|
10
|
+
const SP_CATEGORY := "movement"
|
|
11
|
+
|
|
12
|
+
## Emitted when a dash starts.
|
|
13
|
+
signal dashed
|
|
14
|
+
|
|
15
|
+
## Top speed (px/s).
|
|
16
|
+
@export_range(0.0, 2000.0, 1.0) var max_speed: float = 240.0
|
|
17
|
+
## Acceleration toward the input direction (px/s²).
|
|
18
|
+
@export_range(0.0, 20000.0, 10.0) var acceleration: float = 2400.0
|
|
19
|
+
## Deceleration when there is no input (px/s²).
|
|
20
|
+
@export_range(0.0, 20000.0, 10.0) var friction: float = 2800.0
|
|
21
|
+
## Dash speed (px/s). 0 disables dashing.
|
|
22
|
+
@export_range(0.0, 3000.0, 1.0) var dash_speed: float = 0.0
|
|
23
|
+
## Dash duration in seconds.
|
|
24
|
+
@export_range(0.02, 1.0, 0.01) var dash_duration: float = 0.12
|
|
25
|
+
## Seconds between dashes.
|
|
26
|
+
@export_range(0.0, 5.0, 0.01) var dash_cooldown: float = 0.5
|
|
27
|
+
|
|
28
|
+
## Intent: desired direction, length 0..1.
|
|
29
|
+
var move_vector := Vector2.ZERO
|
|
30
|
+
## Last non-zero movement direction.
|
|
31
|
+
var facing_dir := Vector2.RIGHT
|
|
32
|
+
## Compatibility with platformer code paths: sign of facing_dir.x.
|
|
33
|
+
var facing := 1
|
|
34
|
+
|
|
35
|
+
var _body: CharacterBody2D
|
|
36
|
+
var _dash_requested := false
|
|
37
|
+
var _dash_left := 0.0
|
|
38
|
+
var _dash_cd := 0.0
|
|
39
|
+
var _dash_dir := Vector2.RIGHT
|
|
40
|
+
var _control_lock := 0.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
func _ready() -> void:
|
|
44
|
+
_body = get_parent() as CharacterBody2D
|
|
45
|
+
if _body:
|
|
46
|
+
_body.motion_mode = CharacterBody2D.MOTION_MODE_FLOATING
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Asks for a dash in the movement (or facing) direction.
|
|
50
|
+
func request_dash() -> void:
|
|
51
|
+
_dash_requested = true
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Pushes the entity; control is reduced briefly.
|
|
55
|
+
func apply_knockback(impulse: Vector2) -> void:
|
|
56
|
+
if _body == null:
|
|
57
|
+
return
|
|
58
|
+
_body.velocity = impulse
|
|
59
|
+
_control_lock = 0.15
|
|
60
|
+
_dash_left = 0.0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## True while dashing.
|
|
64
|
+
func is_dashing() -> bool:
|
|
65
|
+
return _dash_left > 0.0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
func _physics_process(delta: float) -> void:
|
|
69
|
+
if _body == null:
|
|
70
|
+
return
|
|
71
|
+
_body.velocity = step_velocity(_body.velocity, delta)
|
|
72
|
+
_body.move_and_slide()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## Pure movement step: returns the new velocity.
|
|
76
|
+
func step_velocity(v: Vector2, delta: float) -> Vector2:
|
|
77
|
+
_dash_cd -= delta
|
|
78
|
+
_control_lock -= delta
|
|
79
|
+
var input := move_vector.limit_length(1.0)
|
|
80
|
+
if input.length() > 0.1:
|
|
81
|
+
facing_dir = input.normalized()
|
|
82
|
+
if absf(facing_dir.x) > 0.1:
|
|
83
|
+
facing = 1 if facing_dir.x > 0.0 else -1
|
|
84
|
+
|
|
85
|
+
if _dash_requested and dash_speed > 0.0 and _dash_cd <= 0.0 and _dash_left <= 0.0:
|
|
86
|
+
_dash_left = dash_duration
|
|
87
|
+
_dash_cd = dash_cooldown + dash_duration
|
|
88
|
+
_dash_dir = facing_dir
|
|
89
|
+
dashed.emit()
|
|
90
|
+
_dash_requested = false
|
|
91
|
+
if _dash_left > 0.0:
|
|
92
|
+
_dash_left -= delta
|
|
93
|
+
if _dash_left <= 0.0:
|
|
94
|
+
return _dash_dir * max_speed
|
|
95
|
+
return _dash_dir * dash_speed
|
|
96
|
+
|
|
97
|
+
var target := input * max_speed
|
|
98
|
+
var rate := acceleration if input.length() > 0.1 else friction
|
|
99
|
+
if _control_lock > 0.0:
|
|
100
|
+
rate *= 0.15
|
|
101
|
+
return v.move_toward(target, rate * delta)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class_name TriggerZoneComponent
|
|
2
|
+
extends Node
|
|
3
|
+
## An invisible-or-visible zone that fires [signal triggered] when an entity of [member group]
|
|
4
|
+
## enters (exit doors, checkpoints, kill zones, tutorial hints).
|
|
5
|
+
|
|
6
|
+
const SP_NAME := "TriggerZone"
|
|
7
|
+
const SP_HOSTS: Array[String] = ["Area2D"]
|
|
8
|
+
const SP_REQUIRES: Array[String] = []
|
|
9
|
+
const SP_CATEGORY := "core"
|
|
10
|
+
|
|
11
|
+
## Emitted when a matching body enters.
|
|
12
|
+
signal triggered(body: Node)
|
|
13
|
+
|
|
14
|
+
## Only bodies in this group trigger the zone ("player", an entity name, or "" for any).
|
|
15
|
+
@export var group: String = "player"
|
|
16
|
+
## Trigger only the first time.
|
|
17
|
+
@export var once := true
|
|
18
|
+
|
|
19
|
+
var _fired := false
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
func _ready() -> void:
|
|
23
|
+
(get_parent() as Area2D).body_entered.connect(_on_body_entered)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
func _on_body_entered(body: Node) -> void:
|
|
27
|
+
if once and _fired:
|
|
28
|
+
return
|
|
29
|
+
if group != "" and not body.is_in_group(StringName(group)):
|
|
30
|
+
return
|
|
31
|
+
_fired = true
|
|
32
|
+
triggered.emit(body)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
class_name WeaponComponent
|
|
2
|
+
extends Node2D
|
|
3
|
+
## Fires projectile entities. Aim comes from aim_direction (set by Aim or ChaseAI), otherwise
|
|
4
|
+
## from the movement facing. Projectiles inherit the shooter's team.
|
|
5
|
+
|
|
6
|
+
const SP_NAME := "Weapon"
|
|
7
|
+
const SP_HOSTS: Array[String] = []
|
|
8
|
+
const SP_REQUIRES: Array[String] = []
|
|
9
|
+
const SP_INTERNAL: Array[String] = ["current_direction"]
|
|
10
|
+
const SP_CATEGORY := "core"
|
|
11
|
+
## Parameters that must name an entity in the graph.
|
|
12
|
+
const SP_ENTITY_PARAMS: Array[String] = ["projectile"]
|
|
13
|
+
|
|
14
|
+
## Emitted every time the weapon fires.
|
|
15
|
+
signal fired
|
|
16
|
+
|
|
17
|
+
## Entity type spawned as the projectile (must have a Projectile component).
|
|
18
|
+
@export var projectile: String = "Bullet"
|
|
19
|
+
## Shots per second.
|
|
20
|
+
@export_range(0.1, 60.0, 0.1) var fire_rate: float = 6.0
|
|
21
|
+
## Random spread in degrees (total cone width).
|
|
22
|
+
@export_range(0.0, 180.0, 0.5) var spread_deg: float = 4.0
|
|
23
|
+
## Projectiles per shot (shotgun when > 1, spread evenly over spread_deg).
|
|
24
|
+
@export_range(1, 32) var projectile_count: int = 1
|
|
25
|
+
## Spawn distance from the entity center (px).
|
|
26
|
+
@export_range(0.0, 200.0, 1.0) var muzzle_distance: float = 20.0
|
|
27
|
+
## Fire continuously without input (enemies, turrets).
|
|
28
|
+
@export var auto_fire := false
|
|
29
|
+
## Push applied to the shooter opposite to the shot (px/s). 0 = none.
|
|
30
|
+
@export_range(0.0, 2000.0, 1.0) var recoil: float = 0.0
|
|
31
|
+
|
|
32
|
+
## Intent: true while the trigger is held.
|
|
33
|
+
var trigger_held := false
|
|
34
|
+
## Intent: aim direction (zero = use movement facing).
|
|
35
|
+
var aim_direction := Vector2.ZERO
|
|
36
|
+
|
|
37
|
+
var _entity: Node2D
|
|
38
|
+
var _cooldown := 0.0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
func _ready() -> void:
|
|
42
|
+
_entity = SP.entity_of(get_parent()) as Node2D
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
func _physics_process(delta: float) -> void:
|
|
46
|
+
_cooldown -= delta
|
|
47
|
+
if (trigger_held or auto_fire) and _cooldown <= 0.0 and GameState.is_playing():
|
|
48
|
+
fire()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
## Fires immediately (ignores the trigger, respects nothing else).
|
|
52
|
+
func fire() -> void:
|
|
53
|
+
_cooldown = 1.0 / fire_rate
|
|
54
|
+
var dir := current_direction()
|
|
55
|
+
var team := SP.team_of(_entity)
|
|
56
|
+
var n := projectile_count
|
|
57
|
+
for i in n:
|
|
58
|
+
var offset := 0.0
|
|
59
|
+
if n > 1:
|
|
60
|
+
offset = lerpf(-spread_deg * 0.5, spread_deg * 0.5, float(i) / float(n - 1))
|
|
61
|
+
else:
|
|
62
|
+
offset = randf_range(-spread_deg * 0.5, spread_deg * 0.5)
|
|
63
|
+
var d := dir.rotated(deg_to_rad(offset))
|
|
64
|
+
var p := World.spawn(projectile, global_position + d * muzzle_distance, {&"sp_team": team, &"sp_direction": d})
|
|
65
|
+
if p:
|
|
66
|
+
p.rotation = d.angle()
|
|
67
|
+
if recoil > 0.0:
|
|
68
|
+
var mv := SP.movement(_entity)
|
|
69
|
+
if mv and mv.has_method(&"apply_knockback") and mv is TopDownMovementComponent:
|
|
70
|
+
mv.apply_knockback(-dir * recoil)
|
|
71
|
+
fired.emit()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Direction the weapon currently fires in.
|
|
75
|
+
func current_direction() -> Vector2:
|
|
76
|
+
if aim_direction.length() > 0.01:
|
|
77
|
+
return aim_direction.normalized()
|
|
78
|
+
var mv := SP.movement(_entity)
|
|
79
|
+
if mv is TopDownMovementComponent:
|
|
80
|
+
return (mv as TopDownMovementComponent).facing_dir
|
|
81
|
+
if mv:
|
|
82
|
+
return Vector2(mv.facing, 0)
|
|
83
|
+
return Vector2.RIGHT
|