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.
Files changed (118) hide show
  1. spawnpoint/__init__.py +3 -0
  2. spawnpoint/_data/examples/coin_hopper.json +228 -0
  3. spawnpoint/_data/examples/neon_arena.json +251 -0
  4. spawnpoint/_data/examples/pixel_arena.json +242 -0
  5. spawnpoint/_data/examples/pixel_hopper.json +221 -0
  6. spawnpoint/_data/godot/addons/spawn_point/components/aim_component.gd +63 -0
  7. spawnpoint/_data/godot/addons/spawn_point/components/animator_component.gd +66 -0
  8. spawnpoint/_data/godot/addons/spawn_point/components/chase_ai_component.gd +107 -0
  9. spawnpoint/_data/godot/addons/spawn_point/components/collectible_component.gd +58 -0
  10. spawnpoint/_data/godot/addons/spawn_point/components/gate_component.gd +64 -0
  11. spawnpoint/_data/godot/addons/spawn_point/components/health_component.gd +83 -0
  12. spawnpoint/_data/godot/addons/spawn_point/components/hitbox_component.gd +50 -0
  13. spawnpoint/_data/godot/addons/spawn_point/components/hurtbox_component.gd +48 -0
  14. spawnpoint/_data/godot/addons/spawn_point/components/juice_component.gd +219 -0
  15. spawnpoint/_data/godot/addons/spawn_point/components/patrol_ai_component.gd +63 -0
  16. spawnpoint/_data/godot/addons/spawn_point/components/platformer_movement_component.gd +204 -0
  17. spawnpoint/_data/godot/addons/spawn_point/components/player_input_component.gd +77 -0
  18. spawnpoint/_data/godot/addons/spawn_point/components/projectile_component.gd +64 -0
  19. spawnpoint/_data/godot/addons/spawn_point/components/spawner_component.gd +58 -0
  20. spawnpoint/_data/godot/addons/spawn_point/components/stomp_component.gd +56 -0
  21. spawnpoint/_data/godot/addons/spawn_point/components/top_down_movement_component.gd +101 -0
  22. spawnpoint/_data/godot/addons/spawn_point/components/trigger_zone_component.gd +32 -0
  23. spawnpoint/_data/godot/addons/spawn_point/components/weapon_component.gd +83 -0
  24. spawnpoint/_data/godot/addons/spawn_point/manifest.json +1509 -0
  25. spawnpoint/_data/godot/addons/spawn_point/runtime/camera_rig.gd +71 -0
  26. spawnpoint/_data/godot/addons/spawn_point/runtime/cockpit.gd +62 -0
  27. spawnpoint/_data/godot/addons/spawn_point/runtime/entity_core.gd +98 -0
  28. spawnpoint/_data/godot/addons/spawn_point/runtime/fx.gd +112 -0
  29. spawnpoint/_data/godot/addons/spawn_point/runtime/game_state.gd +111 -0
  30. spawnpoint/_data/godot/addons/spawn_point/runtime/hud.gd +155 -0
  31. spawnpoint/_data/godot/addons/spawn_point/runtime/level.gd +19 -0
  32. spawnpoint/_data/godot/addons/spawn_point/runtime/sfx.gd +58 -0
  33. spawnpoint/_data/godot/addons/spawn_point/runtime/sp.gd +121 -0
  34. spawnpoint/_data/godot/addons/spawn_point/runtime/world.gd +130 -0
  35. spawnpoint/_data/godot/addons/spawn_point/shaders/flash.gdshader +9 -0
  36. spawnpoint/_data/godot/addons/spawn_point/shaders/floor_grid.gdshader +13 -0
  37. spawnpoint/_data/godot/addons/spawn_point/tools/build.gd +612 -0
  38. spawnpoint/_data/godot/addons/spawn_point/tools/build.tscn +6 -0
  39. spawnpoint/_data/godot/addons/spawn_point/tools/capture.gd +42 -0
  40. spawnpoint/_data/godot/addons/spawn_point/tools/capture.tscn +6 -0
  41. spawnpoint/_data/godot/addons/spawn_point/tools/dump_manifest.gd +91 -0
  42. spawnpoint/_data/godot/addons/spawn_point/tools/dump_manifest.tscn +6 -0
  43. spawnpoint/_data/godot/addons/spawn_point/tools/playtest.gd +428 -0
  44. spawnpoint/_data/godot/addons/spawn_point/tools/playtest.tscn +6 -0
  45. spawnpoint/_data/godot/addons/spawn_point/tools/playtest_nav.gd +152 -0
  46. spawnpoint/_data/godot/addons/spawn_point/tools/record.gd +89 -0
  47. spawnpoint/_data/godot/addons/spawn_point/tools/record.tscn +6 -0
  48. spawnpoint/_data/godot/addons/spawn_point/tools/smoke.gd +72 -0
  49. spawnpoint/_data/godot/addons/spawn_point/tools/smoke.tscn +6 -0
  50. spawnpoint/analyze.py +387 -0
  51. spawnpoint/build.py +146 -0
  52. spawnpoint/cli.py +290 -0
  53. spawnpoint/cockpit/__init__.py +0 -0
  54. spawnpoint/cockpit/server.py +473 -0
  55. spawnpoint/cockpit/static/app.css +111 -0
  56. spawnpoint/cockpit/static/app.js +327 -0
  57. spawnpoint/cockpit/static/index.html +79 -0
  58. spawnpoint/diagnostics.py +91 -0
  59. spawnpoint/expr.py +219 -0
  60. spawnpoint/gif.py +58 -0
  61. spawnpoint/godot.py +107 -0
  62. spawnpoint/graph.py +175 -0
  63. spawnpoint/level.py +157 -0
  64. spawnpoint/llm_server.py +77 -0
  65. spawnpoint/manifest.py +222 -0
  66. spawnpoint/playtest/__init__.py +104 -0
  67. spawnpoint/playtest/report.py +148 -0
  68. spawnpoint/playtest/runner.py +51 -0
  69. spawnpoint/playtest/tuner.py +151 -0
  70. spawnpoint/project.py +94 -0
  71. spawnpoint/py.typed +0 -0
  72. spawnpoint/rules.py +310 -0
  73. spawnpoint/sfx.py +352 -0
  74. spawnpoint/sprites/__init__.py +0 -0
  75. spawnpoint/sprites/ai.py +222 -0
  76. spawnpoint/sprites/contour.py +147 -0
  77. spawnpoint/sprites/generate.py +507 -0
  78. spawnpoint/sprites/importer.py +212 -0
  79. spawnpoint/sprites/resolve.py +139 -0
  80. spawnpoint/sprites/spriteset.py +159 -0
  81. spawnpoint/sprites/tiles.py +102 -0
  82. spawnpoint/studio/__init__.py +0 -0
  83. spawnpoint/studio/assemble.py +528 -0
  84. spawnpoint/studio/brief.py +197 -0
  85. spawnpoint/studio/creative.py +124 -0
  86. spawnpoint/studio/dataset.py +136 -0
  87. spawnpoint/studio/edits.py +206 -0
  88. spawnpoint/studio/levelgen.py +352 -0
  89. spawnpoint/studio/llm.py +145 -0
  90. spawnpoint/studio/orchestrator.py +341 -0
  91. spawnpoint/studio/prompts.py +122 -0
  92. spawnpoint/studio/web.py +123 -0
  93. spawnpoint/studio/webcheck.py +210 -0
  94. spawnpoint/studio_cli.py +232 -0
  95. spawnpoint_studio-0.1.0.dist-info/METADATA +360 -0
  96. spawnpoint_studio-0.1.0.dist-info/RECORD +118 -0
  97. spawnpoint_studio-0.1.0.dist-info/WHEEL +5 -0
  98. spawnpoint_studio-0.1.0.dist-info/entry_points.txt +2 -0
  99. spawnpoint_studio-0.1.0.dist-info/licenses/LICENSE +21 -0
  100. spawnpoint_studio-0.1.0.dist-info/top_level.txt +2 -0
  101. tests/conftest.py +49 -0
  102. tests/test_ai_sprites.py +122 -0
  103. tests/test_analyze.py +116 -0
  104. tests/test_cockpit.py +148 -0
  105. tests/test_e2e.py +33 -0
  106. tests/test_expr.py +59 -0
  107. tests/test_gif.py +19 -0
  108. tests/test_godot_library.py +17 -0
  109. tests/test_graph.py +44 -0
  110. tests/test_level.py +64 -0
  111. tests/test_live_llm.py +38 -0
  112. tests/test_manifest.py +54 -0
  113. tests/test_playtest.py +129 -0
  114. tests/test_rules.py +85 -0
  115. tests/test_sfx.py +30 -0
  116. tests/test_sprites.py +261 -0
  117. tests/test_studio.py +335 -0
  118. 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