diff --git a/addons/fogwaves_exports/inspectors.gd b/addons/fogwaves_exports/inspectors.gd new file mode 100644 index 0000000..25741fc --- /dev/null +++ b/addons/fogwaves_exports/inspectors.gd @@ -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) diff --git a/addons/fogwaves_exports/inspectors.gd.uid b/addons/fogwaves_exports/inspectors.gd.uid new file mode 100644 index 0000000..a6e2d16 --- /dev/null +++ b/addons/fogwaves_exports/inspectors.gd.uid @@ -0,0 +1 @@ +uid://bp816dbdpjcae diff --git a/addons/fogwaves_exports/plugin.cfg b/addons/fogwaves_exports/plugin.cfg new file mode 100644 index 0000000..2f77d1d --- /dev/null +++ b/addons/fogwaves_exports/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Fogwaves Export Helpers" +description="Various custom inspector plugins." +author="fogwaves" +version="" +script="plugin.gd" diff --git a/addons/fogwaves_exports/plugin.gd b/addons/fogwaves_exports/plugin.gd new file mode 100644 index 0000000..c012036 --- /dev/null +++ b/addons/fogwaves_exports/plugin.gd @@ -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) diff --git a/addons/fogwaves_exports/plugin.gd.uid b/addons/fogwaves_exports/plugin.gd.uid new file mode 100644 index 0000000..4de0993 --- /dev/null +++ b/addons/fogwaves_exports/plugin.gd.uid @@ -0,0 +1 @@ +uid://mgh5g40ugy83 diff --git a/project.godot b/project.godot index 9e2b1ff..b214ee4 100644 --- a/project.godot +++ b/project.godot @@ -28,12 +28,18 @@ window/size/viewport_width=240 window/size/viewport_height=320 window/vsync/vsync_mode=0 +[editor_plugins] + +enabled=PackedStringArray("res://addons/fogwaves_exports/plugin.cfg") + [file_customization] folder_colors={ +"res://addons/": "gray", "res://globals/": "orange", "res://objects/": "blue", -"res://scenes/": "teal" +"res://scenes/": "teal", +"res://systems/": "red" } [input] diff --git a/scenes/test_scene_haze.tscn b/scenes/test_scene_haze.tscn index 29c4f71..9c5d781 100644 --- a/scenes/test_scene_haze.tscn +++ b/scenes/test_scene_haze.tscn @@ -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="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="Texture2D" uid="uid://xe124f1kgf3x" path="res://graphics/bullets/normal_bullet/bullet_2.png" id="4_hlyn7"] +[ext_resource type="Script" uid="uid://cj2fj7snls8aa" path="res://systems/bullets/bullet_set.gd" id="3_cf1so"] +[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"] @@ -14,9 +39,8 @@ texture = ExtResource("1_g7g4h") [node name="Player" parent="." instance=ExtResource("2_j8ivh")] position = Vector2(100, 99) -[node name="Bullet" type="Area2D" parent="."] -position = Vector2(169, 130) -script = ExtResource("3_hlyn7") -texture = ExtResource("4_hlyn7") -hitbox_size = Vector2i(6, 6) -metadata/_custom_type_script = "uid://ntpaank0h0a0" +[node name="BulletSet" type="Node2D" parent="."] +position = Vector2(110, 192) +script = ExtResource("3_cf1so") +preset = SubResource("Resource_uu3sg") +metadata/_custom_type_script = "uid://cj2fj7snls8aa" diff --git a/systems/bullets/behaviors/bullet_behavior.gd b/systems/bullets/behaviors/bullet_behavior.gd new file mode 100644 index 0000000..1c38dc8 --- /dev/null +++ b/systems/bullets/behaviors/bullet_behavior.gd @@ -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 diff --git a/systems/bullets/behaviors/bullet_behavior.gd.uid b/systems/bullets/behaviors/bullet_behavior.gd.uid new file mode 100644 index 0000000..72c30cf --- /dev/null +++ b/systems/bullets/behaviors/bullet_behavior.gd.uid @@ -0,0 +1 @@ +uid://djcajenyac4sd diff --git a/systems/bullets/behaviors/simple_linear_behavior.gd b/systems/bullets/behaviors/simple_linear_behavior.gd new file mode 100644 index 0000000..c050f56 --- /dev/null +++ b/systems/bullets/behaviors/simple_linear_behavior.gd @@ -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 diff --git a/systems/bullets/behaviors/simple_linear_behavior.gd.uid b/systems/bullets/behaviors/simple_linear_behavior.gd.uid new file mode 100644 index 0000000..fca0d5b --- /dev/null +++ b/systems/bullets/behaviors/simple_linear_behavior.gd.uid @@ -0,0 +1 @@ +uid://dntp60my5f65m diff --git a/systems/bullets/bullet.gd b/systems/bullets/bullet.gd index 254b1f8..20f51b7 100644 --- a/systems/bullets/bullet.gd +++ b/systems/bullets/bullet.gd @@ -1,17 +1,22 @@ @tool +@icon("bullet.svg") class_name Bullet 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): texture = value 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): hitbox_size = value 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.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() +## 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: + monitoring = false + add_to_group(&"bullets") _hitbox.debug_color.a = 0.0 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: - draw_texture(texture, -texture.get_size() * 0.5) + if texture: + draw_texture(texture, -texture.get_size() * 0.5) func _get_configuration_warnings() -> PackedStringArray: 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() diff --git a/systems/bullets/bullet.svg b/systems/bullets/bullet.svg new file mode 100644 index 0000000..6a457a1 --- /dev/null +++ b/systems/bullets/bullet.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + diff --git a/systems/bullets/bullet.svg.import b/systems/bullets/bullet.svg.import new file mode 100644 index 0000000..2a0458c --- /dev/null +++ b/systems/bullets/bullet.svg.import @@ -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 diff --git a/systems/bullets/bullet_preset.gd b/systems/bullets/bullet_preset.gd new file mode 100644 index 0000000..8ff6541 --- /dev/null +++ b/systems/bullets/bullet_preset.gd @@ -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 diff --git a/systems/bullets/bullet_preset.gd.uid b/systems/bullets/bullet_preset.gd.uid new file mode 100644 index 0000000..0269ea3 --- /dev/null +++ b/systems/bullets/bullet_preset.gd.uid @@ -0,0 +1 @@ +uid://vus1a0flwtnm diff --git a/systems/bullets/bullet_set.gd b/systems/bullets/bullet_set.gd new file mode 100644 index 0000000..d98573a --- /dev/null +++ b/systems/bullets/bullet_set.gd @@ -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) diff --git a/systems/bullets/bullet_set.gd.uid b/systems/bullets/bullet_set.gd.uid new file mode 100644 index 0000000..891b9ca --- /dev/null +++ b/systems/bullets/bullet_set.gd.uid @@ -0,0 +1 @@ +uid://cj2fj7snls8aa diff --git a/systems/bullets/spawn_patterns/bullet_spawn_pattern.gd b/systems/bullets/spawn_patterns/bullet_spawn_pattern.gd new file mode 100644 index 0000000..be4e178 --- /dev/null +++ b/systems/bullets/spawn_patterns/bullet_spawn_pattern.gd @@ -0,0 +1,6 @@ +@abstract +class_name BulletSpawnPattern +extends Resource + + +@abstract func spawn_bullets(bullet_set: BulletSet, preset: BulletPreset) -> void diff --git a/systems/bullets/spawn_patterns/bullet_spawn_pattern.gd.uid b/systems/bullets/spawn_patterns/bullet_spawn_pattern.gd.uid new file mode 100644 index 0000000..bd82cb2 --- /dev/null +++ b/systems/bullets/spawn_patterns/bullet_spawn_pattern.gd.uid @@ -0,0 +1 @@ +uid://bhy0mkwfsi5j8 diff --git a/systems/bullets/spawn_patterns/ring_pattern.gd b/systems/bullets/spawn_patterns/ring_pattern.gd new file mode 100644 index 0000000..da695ee --- /dev/null +++ b/systems/bullets/spawn_patterns/ring_pattern.gd @@ -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) diff --git a/systems/bullets/spawn_patterns/ring_pattern.gd.uid b/systems/bullets/spawn_patterns/ring_pattern.gd.uid new file mode 100644 index 0000000..322a174 --- /dev/null +++ b/systems/bullets/spawn_patterns/ring_pattern.gd.uid @@ -0,0 +1 @@ +uid://dtuc6qerbfset