yes, they canjj st!

This commit is contained in:
Haze Weathers 2025-12-14 17:06:42 -06:00
parent 6b92473eeb
commit a6421235f8
22 changed files with 510 additions and 14 deletions

View file

@ -0,0 +1,121 @@
extends EditorInspectorPlugin
func _can_handle(object: Object) -> bool:
return true
func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
if type == TYPE_VECTOR2 and hint_string == "direction":
add_property_editor(name, DirectionInspector.new(), false)
return true
return false
class DirectionInspector extends EditorProperty:
var angle_selector: AngleSelector
var snap_toggle: TextureButton
var snap_spinbox: SpinBox
var current_value: Vector2
var updating := false
var snap := false
var snap_amount: int = 0
func _init() -> void:
var hbox = HBoxContainer.new()
hbox.alignment = BoxContainer.ALIGNMENT_BEGIN
add_child(hbox)
var vbox = VBoxContainer.new()
vbox.alignment = BoxContainer.ALIGNMENT_BEGIN
vbox.add_theme_constant_override(&"separation", 4)
vbox.add_spacer(true)
hbox.add_child(vbox)
snap_toggle = TextureButton.new()
snap_toggle.toggle_mode = true
snap_toggle.toggled.connect(_on_snap_toggled)
snap_toggle.texture_normal = EditorInterface.get_editor_theme().get_icon(&"Snap", &"EditorIcons")
snap_toggle.texture_pressed = snap_toggle.texture_normal
snap_toggle.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
snap_toggle.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT
vbox.add_child(snap_toggle)
snap_spinbox = SpinBox.new()
snap_spinbox.editable = false
snap_spinbox.rounded = true
snap_spinbox.value_changed.connect(_on_snap_amount_changed)
snap_spinbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
snap_spinbox.custom_minimum_size = Vector2.ZERO
vbox.add_child(snap_spinbox)
angle_selector = AngleSelector.new()
angle_selector.direction_clicked.connect(_on_direction_clicked)
hbox.add_child(angle_selector)
add_focusable(angle_selector)
custom_minimum_size = get_combined_minimum_size()
size_flags_horizontal = Control.SIZE_EXPAND_FILL
_refresh()
func _update_property() -> void:
var new_value: Vector2 = get_edited_object()[get_edited_property()]
if new_value == current_value:
return
updating = true
current_value = new_value
_refresh()
updating = false
func _refresh() -> void:
angle_selector.direction = current_value
func _on_direction_clicked(direction: Vector2) -> void:
if updating:
return
current_value = direction
if snap and snap_amount > 0:
var angle = direction.angle()
var steps = PI * 0.5 / float(snap_amount)
angle = roundf(angle / steps) * steps
current_value = Vector2.from_angle(angle)
_refresh()
emit_changed(get_edited_property(), current_value)
func _on_snap_toggled(toggled_on: bool) -> void:
snap = toggled_on
snap_spinbox.editable = snap
if toggled_on:
snap_toggle.modulate = EditorInterface.get_editor_theme().get_color(&"accent_color", &"Editor")
else:
snap_toggle.modulate = Color.WHITE
func _on_snap_amount_changed(value: float) -> void:
snap_amount = maxi(0, int(value))
class AngleSelector extends Control:
signal direction_clicked(direction: Vector2)
var direction: Vector2:
set(value):
direction = value.normalized()
queue_redraw()
func _init() -> void:
custom_minimum_size = Vector2(48.0, 48.0)
focus_mode = Control.FOCUS_CLICK
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton or event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
direction_clicked.emit((size * 0.5).direction_to(get_local_mouse_position()))
func _draw() -> void:
var center = size * 0.5
var radius = minf(center.x, center.y)
draw_circle(center, radius, Color.WHITE, false)
draw_line(center, center + direction * radius, Color.RED, 2.0)

View file

@ -0,0 +1 @@
uid://bp816dbdpjcae

View file

@ -0,0 +1,7 @@
[plugin]
name="Fogwaves Export Helpers"
description="Various custom inspector plugins."
author="fogwaves"
version=""
script="plugin.gd"

View file

@ -0,0 +1,17 @@
@tool
extends EditorPlugin
const Inspectors = preload("inspectors.gd")
var inspector_plugin: Inspectors
func _enter_tree() -> void:
inspector_plugin = Inspectors.new()
add_inspector_plugin(inspector_plugin)
func _exit_tree() -> void:
remove_inspector_plugin(inspector_plugin)

View file

@ -0,0 +1 @@
uid://mgh5g40ugy83

View file

@ -28,12 +28,18 @@ window/size/viewport_width=240
window/size/viewport_height=320 window/size/viewport_height=320
window/vsync/vsync_mode=0 window/vsync/vsync_mode=0
[editor_plugins]
enabled=PackedStringArray("res://addons/fogwaves_exports/plugin.cfg")
[file_customization] [file_customization]
folder_colors={ folder_colors={
"res://addons/": "gray",
"res://globals/": "orange", "res://globals/": "orange",
"res://objects/": "blue", "res://objects/": "blue",
"res://scenes/": "teal" "res://scenes/": "teal",
"res://systems/": "red"
} }
[input] [input]

View file

@ -1,9 +1,34 @@
[gd_scene load_steps=5 format=3 uid="uid://dxsp66qpvm65b"] [gd_scene load_steps=11 format=3 uid="uid://dxsp66qpvm65b"]
[ext_resource type="Texture2D" uid="uid://c50bfqprpitev" path="res://icon.svg" id="1_g7g4h"] [ext_resource type="Texture2D" uid="uid://c50bfqprpitev" path="res://icon.svg" id="1_g7g4h"]
[ext_resource type="PackedScene" uid="uid://c714s5d7d5765" path="res://objects/player/player.tscn" id="2_j8ivh"] [ext_resource type="PackedScene" uid="uid://c714s5d7d5765" path="res://objects/player/player.tscn" id="2_j8ivh"]
[ext_resource type="Script" uid="uid://ntpaank0h0a0" path="res://systems/bullets/bullet.gd" id="3_hlyn7"] [ext_resource type="Script" uid="uid://cj2fj7snls8aa" path="res://systems/bullets/bullet_set.gd" id="3_cf1so"]
[ext_resource type="Texture2D" uid="uid://xe124f1kgf3x" path="res://graphics/bullets/normal_bullet/bullet_2.png" id="4_hlyn7"] [ext_resource type="Texture2D" uid="uid://du7gh3nk66mpo" path="res://graphics/bullets/normal_bullet/bullet_1.png" id="4_hlyn7"]
[ext_resource type="Script" uid="uid://dntp60my5f65m" path="res://systems/bullets/behaviors/simple_linear_behavior.gd" id="4_t1bs8"]
[ext_resource type="Script" uid="uid://dtuc6qerbfset" path="res://systems/bullets/spawn_patterns/ring_pattern.gd" id="5_4oowd"]
[ext_resource type="Script" uid="uid://vus1a0flwtnm" path="res://systems/bullets/bullet_preset.gd" id="6_sle1e"]
[sub_resource type="Resource" id="Resource_c0i5a"]
script = ExtResource("4_t1bs8")
acceleration = 64.0
metadata/_custom_type_script = "uid://dntp60my5f65m"
[sub_resource type="Resource" id="Resource_1xo0o"]
script = ExtResource("5_4oowd")
bullet_count = 5
distance_offset = 16.0
direction_rotation = 3.9269908169872414
metadata/_custom_type_script = "uid://dtuc6qerbfset"
[sub_resource type="Resource" id="Resource_uu3sg"]
script = ExtResource("6_sle1e")
behavior = SubResource("Resource_c0i5a")
pattern = SubResource("Resource_1xo0o")
textures = Array[Texture2D]([ExtResource("4_hlyn7")])
hitbox_size = Vector2i(6, 6)
rounds = 5
round_delay = 1.0
metadata/_custom_type_script = "uid://vus1a0flwtnm"
[node name="TestScene" type="Node"] [node name="TestScene" type="Node"]
@ -14,9 +39,8 @@ texture = ExtResource("1_g7g4h")
[node name="Player" parent="." instance=ExtResource("2_j8ivh")] [node name="Player" parent="." instance=ExtResource("2_j8ivh")]
position = Vector2(100, 99) position = Vector2(100, 99)
[node name="Bullet" type="Area2D" parent="."] [node name="BulletSet" type="Node2D" parent="."]
position = Vector2(169, 130) position = Vector2(110, 192)
script = ExtResource("3_hlyn7") script = ExtResource("3_cf1so")
texture = ExtResource("4_hlyn7") preset = SubResource("Resource_uu3sg")
hitbox_size = Vector2i(6, 6) metadata/_custom_type_script = "uid://cj2fj7snls8aa"
metadata/_custom_type_script = "uid://ntpaank0h0a0"

View file

@ -0,0 +1,12 @@
@abstract
class_name BulletBehavior
extends Resource
## Called when a bullet is spawned with this behavior in order to set up
## behavior-specific state.
@warning_ignore("unused_parameter")
func init_bullet(bullet: Bullet) -> void: pass
## Called to process a tick of a bullet's movement.
@abstract func process_bullet(bullet: Bullet, delta: float) -> void

View file

@ -0,0 +1 @@
uid://djcajenyac4sd

View file

@ -0,0 +1,15 @@
class_name SimpleLinearBehavior
extends BulletBehavior
## Makes bullets move in [member Bullet.direction], potentially accelerating.
## Initial speed of the bullet when it is spawned.
@export_custom(0, "suffix:px/s") var initial_speed: float = 0.0
## Rate at which the bullet will speed up.
@export_custom(0, "suffix:px/s²") var acceleration: float = 0.0
func process_bullet(bullet: Bullet, delta: float) -> void:
var speed = initial_speed + acceleration * bullet.time_elapsed
bullet.position += bullet.direction * speed * delta

View file

@ -0,0 +1 @@
uid://dntp60my5f65m

View file

@ -1,17 +1,22 @@
@tool @tool
@icon("bullet.svg")
class_name Bullet class_name Bullet
extends Area2D extends Area2D
static var _hitbox_shapes: Dictionary[Vector2i, RectangleShape2D] = {} ## The number of bullets to allocate at startup.
const INITIAL_ALLOCATED_BULLETS: int = 2000
@export var texture: Texture2D: ## Texture to draw for the bullet.
@export var texture: Texture2D = null:
set(value): set(value):
texture = value texture = value
queue_redraw() queue_redraw()
_update_visibility_notifier()
@export var hitbox_size: Vector2i: ## Size of the bullet's collision box.
@export var hitbox_size: Vector2i = Vector2i.ZERO:
set(value): set(value):
hitbox_size = value hitbox_size = value
if not _hitbox_shapes.has(hitbox_size): if not _hitbox_shapes.has(hitbox_size):
@ -20,18 +25,100 @@ static var _hitbox_shapes: Dictionary[Vector2i, RectangleShape2D] = {}
_hitbox_shapes[hitbox_size] = new_shape _hitbox_shapes[hitbox_size] = new_shape
_hitbox.shape = _hitbox_shapes[hitbox_size] _hitbox.shape = _hitbox_shapes[hitbox_size]
## The direction that the bullet is travelling.
@export_custom(0, "direction") var direction: Vector2 = Vector2.RIGHT
## If [code]true[/code], the bullet will always rotate to face toward [member direction].
@export var face_direction: bool = false
## The amount of time in seconds that the bullet has existed.
var time_elapsed: float = 0.0
static var _cached_bullets: Array[Bullet] = []
static var _hitbox_shapes: Dictionary[Vector2i, RectangleShape2D] = {}
var _hitbox: CollisionShape2D = CollisionShape2D.new() var _hitbox: CollisionShape2D = CollisionShape2D.new()
## Returns a new [Bullet], which may be sourced from the cached bullets.
@warning_ignore("shadowed_variable")
static func create(
texture: Texture2D = null,
hitbox_size: Vector2i = Vector2i.ZERO,
direction: Vector2 = Vector2.RIGHT,
face_direction: bool = false,
) -> Bullet:
var bullet: Bullet = _cached_bullets.pop_back()
if not bullet:
bullet = Bullet.new()
bullet.texture = texture
bullet.hitbox_size = hitbox_size
bullet.direction = direction
bullet.face_direction = face_direction
bullet.time_elapsed = 0.0
return bullet
## Removes the bullet from the scene tree and returns it to the bullet cache to be
## re-used later.
func recycle() -> void:
get_parent().remove_child(self)
_cached_bullets.append(self)
static func _static_init() -> void:
for _i in INITIAL_ALLOCATED_BULLETS:
_cached_bullets.append(Bullet.new())
func _init() -> void: func _init() -> void:
monitoring = false
add_to_group(&"bullets")
_hitbox.debug_color.a = 0.0 _hitbox.debug_color.a = 0.0
add_child(_hitbox) add_child(_hitbox)
func _enter_tree() -> void:
_update_visibility_notifier()
func _physics_process(delta: float) -> void:
if not Engine.is_editor_hint():
time_elapsed += delta
if face_direction:
rotation = direction.angle()
func _draw() -> void: func _draw() -> void:
if texture:
draw_texture(texture, -texture.get_size() * 0.5) draw_texture(texture, -texture.get_size() * 0.5)
func _get_configuration_warnings() -> PackedStringArray: func _get_configuration_warnings() -> PackedStringArray:
return [] return []
# sets the canvas item up to notify when it leaves the screen
# this essentially mimics `VisibleOnScreenNotifier` without an additional node
func _update_visibility_notifier() -> void:
if not texture or not is_inside_tree() or Engine.is_editor_hint():
return
# the visibility rect is set to twice the size of the texture to add a little margin
# (func(): pass) is the cleanest way i could think of to have a callback that does nothing
RenderingServer.canvas_item_set_visibility_notifier(
get_canvas_item(), true,
Rect2(-texture.get_size(), texture.get_size() * 2.0),
(func(): pass), _on_screen_exited
)
# called when the bullet leaves the screen.
func _on_screen_exited() -> void:
recycle()

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="bullet.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
id="a"
x2="0"
y1="2"
y2="14"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4)">
<stop
offset="0"
stop-color="#ff8dbc"
id="stop1" />
<stop
offset=".4"
stop-color="#7260ff"
id="stop2" />
<stop
offset=".6"
stop-color="#7260ff"
id="stop3" />
<stop
offset="1"
stop-color="#74c9ff"
id="stop4" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="10.995366"
inkscape:cx="8.7309506"
inkscape:cy="10.049688"
inkscape:window-width="1358"
inkscape:window-height="742"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 1.3983532,8.3266862 6.414829,13.926473 7.737001,12.526527 2.7594126,7.160064 Z"
id="path2"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 3.4711505,6.74088 8.3320767,11.951793 13.465215,7.2075289 C 14.904049,5.69092 16.498433,3.3965627 14.554063,1.5299671 13.270778,-0.10330412 11.326407,0.36334478 9.1098249,2.0743908 Z"
id="path3"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 3.2131098,11.837773 1.056944,14.71266 4.1432206,12.810161 Z"
id="path5"
sodipodi:nodetypes="cccc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 4.8196647,13.40205 2.5789434,15.219994 5.4538311,14.247606 Z"
id="path6" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 1.4797216,10.019829 0.42277761,12.979273 2.3252768,10.992217 Z"
id="path7"
sodipodi:nodetypes="cccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://73kflfycjlft"
path="res://.godot/imported/bullet.svg-bd5de27a67ea13ec7922f62f013cca2b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://systems/bullets/bullet.svg"
dest_files=["res://.godot/imported/bullet.svg-bd5de27a67ea13ec7922f62f013cca2b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,16 @@
class_name BulletPreset
extends Resource
@export var behavior: BulletBehavior = null
@export var pattern: BulletSpawnPattern = null
@export var textures: Array[Texture2D] = []
@export var hitbox_size: Vector2i = Vector2i.ZERO
@export var face_direction: bool = false
@export var rounds: int = 1
@export var round_delay: float = 0.0

View file

@ -0,0 +1 @@
uid://vus1a0flwtnm

View file

@ -0,0 +1,31 @@
class_name BulletSet
extends Node2D
@export var preset: BulletPreset
func _ready() -> void:
for _n in preset.rounds:
preset.pattern.spawn_bullets(self, preset)
await get_tree().create_timer(preset.round_delay, false, true).timeout
func _physics_process(delta: float) -> void:
for bullet in get_children():
if bullet is Bullet:
preset.behavior.process_bullet(bullet, delta)
else:
push_error("BulletSet does not support having non-bullet children. Removing child: ", bullet)
bullet.queue_free()
func add_bullet(bullet: Bullet) -> void:
preset.behavior.init_bullet(bullet)
add_child(bullet)
func add_bullets(new_bullets: Array[Bullet]) -> void:
for bullet in new_bullets:
preset.behavior.init_bullet(bullet)
add_child(bullet)

View file

@ -0,0 +1 @@
uid://cj2fj7snls8aa

View file

@ -0,0 +1,6 @@
@abstract
class_name BulletSpawnPattern
extends Resource
@abstract func spawn_bullets(bullet_set: BulletSet, preset: BulletPreset) -> void

View file

@ -0,0 +1 @@
uid://bhy0mkwfsi5j8

View file

@ -0,0 +1,21 @@
class_name RingPattern
extends BulletSpawnPattern
@export var bullet_count: int
@export var distance_offset: float
@export_custom(0, "radians_as_degrees") var angle_offset: float
@export_custom(0, "radians_as_degrees") var direction_rotation: float
func spawn_bullets(bullet_set: BulletSet, preset: BulletPreset) -> void:
for i in bullet_count:
var angle = (float(i) / float(bullet_count)) * TAU + angle_offset
var bullet = Bullet.create(
preset.textures.pick_random(),
preset.hitbox_size,
Vector2.from_angle(angle + direction_rotation),
preset.face_direction
)
bullet.position = Vector2.from_angle(angle) * distance_offset
bullet_set.add_bullet(bullet)

View file

@ -0,0 +1 @@
uid://dtuc6qerbfset