Compare commits

...

14 Commits

Author SHA1 Message Date
356897045f Merge pull request 'chore: add flake check workflow' (#21) from workflow-init into master
All checks were successful
flake-check / build (push) Successful in 46s
Reviewed-on: #21
2025-08-27 21:08:53 +00:00
1e1f8cebe8 chore: add flake check workflow
All checks were successful
flake-check / build (pull_request) Successful in 48s
2025-08-27 23:05:32 +02:00
b21ad67cfc chore: add gdformat check to flake 2025-08-26 21:51:38 +02:00
4fffa8784a chore: remove version from image name 2025-08-26 20:23:43 +02:00
dc3f8c94f8 Merge pull request 'Add minimal main menu' (#19) from 15-main-menu into master
Reviewed-on: #19
2025-08-23 19:06:03 +00:00
f06bf17757 game: add minimal main menu 2025-08-23 21:04:26 +02:00
d74831df56 chore: gdformat bleed changes 2025-08-23 20:52:29 +02:00
2bd83504f2 game: remove raycast added to enemy group by mistake 2025-08-23 20:51:13 +02:00
3f21aef4eb game: make blead deal damage 2025-08-22 20:56:06 +02:00
6fff2dd9a3 game: add bleed 2025-08-22 20:50:11 +02:00
0dada63709 assets: add bleed icon sprite 2025-08-22 20:50:04 +02:00
d862974747 assets: add puddle sprite 2025-08-22 15:21:45 +02:00
f53d91a9eb game: cache some calculations for enemies 2025-08-22 14:12:42 +02:00
d57a59e9fe game: make some changes to water shader 2025-08-22 10:19:55 +02:00
30 changed files with 442 additions and 50 deletions

16
.github/workflows/flake-check.yaml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: flake-check
on:
push:
branches: master
pull_request:
branches:
- master
jobs:
build:
runs-on: [ ubuntu-latest, homelab ]
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v31
- run: nix flake check

View File

@@ -3,19 +3,28 @@ uniform vec4 water_color : source_color = vec4(0.0, 0.0, 1.0, 1.0);
uniform sampler2D noise_tex; // noise texture
uniform vec2 noise_speed = vec2(0.05, 0.01);
uniform float noise_strength = 0.2; // how much to lighten/darken
uniform float pixel_size = 0.005;
void fragment() {
vec2 uv = UV;
vec4 base_tex = texture(TEXTURE, uv);
float is_water = step(0.8, 1.0 - distance(base_tex.rgb, water_color.rgb));
vec2 snapped_uv = floor(SCREEN_UV / pixel_size) * pixel_size;
// Scroll UVs for noise animation
vec2 noise_uv = SCREEN_UV + noise_speed * TIME;
//vec2 noise_uv = SCREEN_UV + noise_speed * TIME;
vec2 noise_uv = snapped_uv + noise_speed * TIME;
vec4 noise_sample = texture(noise_tex, fract(noise_uv));
float n = noise_sample.r;
n = pow(n, 3.0) * 4.0;
n = clamp(n,0.0, 1.0);
vec4 noisy_color = water_color.rgba;
noisy_color += vec4(0.1* (noise_sample.r - 0.5), 0.05 * (noise_sample.r - 0.5), 0.0, 1.0);
// Use noise (0..1) to brighten/darken the water color
float brightness = (noise_sample.r - 0.5) * 2.0 * noise_strength;
vec4 animated_water = water_color + vec4(vec3(brightness), 0.0);
float brightness = (n - 0.5) * 4.0 * noise_strength;
vec4 animated_water = noisy_color + vec4(vec3(brightness), 0.0);
COLOR = mix(base_tex, animated_water, is_water);

BIN
assets/sprites/puddle_1.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/puddle_1.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c5t4it4if0s6g"
path="res://.godot/imported/puddle_1.png-28d62b8b3cc5647c5219303699bcce62.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/puddle_1.png"
dest_files=["res://.godot/imported/puddle_1.png-28d62b8b3cc5647c5219303699bcce62.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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

BIN
assets/sprites/small_bleed_icon.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/sprites/small_bleed_icon.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c856sh6vk5lqa"
path="res://.godot/imported/small_bleed_icon.png-472f2c7cb608bb835a947b6c2d78acf7.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/sprites/small_bleed_icon.png"
dest_files=["res://.godot/imported/small_bleed_icon.png-472f2c7cb608bb835a947b6c2d78acf7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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

View File

@@ -3,7 +3,7 @@
name="Linux"
platform="Linux"
runnable=true
advanced_options=false
advanced_options=true
dedicated_server=false
custom_features=""
export_filter="all_resources"

View File

@@ -46,6 +46,30 @@
}
);
checks = forAllSystems (
{ pkgs }:
{
gdformat = pkgs.stdenvNoCC.mkDerivation {
name = "gdformat-check";
src = pkgs.lib.sources.sourceFilesBySuffices (pkgs.lib.cleanSource ./.) [ ".gd" ];
nativeBuildInputs = with pkgs; [
gdtoolkit_4
];
dontBuild = true;
doCheck = true;
checkPhase = ''
export HOME=$(mktemp -d)
find . -name "*.gd" -print0 | xargs -0 gdformat --check
echo "All .gd files are properly formatted"
'';
installPhase = "mkdir $out";
};
}
);
packages = forAllSystems (
{ pkgs }:
let
@@ -77,7 +101,6 @@
runHook postBuild
'';
installPhase = ''
find .
install -D -m 755 -t $out/libexec ./build/slopvivors
install -D -m 644 -t $out/libexec ./build/slopvivors.pck
install -d -m 755 $out/bin
@@ -158,7 +181,7 @@
'';
};
slopvivors_docker = pkgs.dockerTools.buildLayeredImage {
name = "slopvivors-docker-${version}";
name = "slopvivors-docker";
tag = version;
created = "now";
contents = [

View File

@@ -0,0 +1,8 @@
class_name EnemyEffectBase
extends Resource
var enemy: EnemyBase
func apply(enemy: EnemyBase) -> void:
push_error("%s does not implement apply" % self)

View File

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

View File

@@ -0,0 +1,33 @@
class_name EnemyEffectBleed
extends EnemyEffectBase
var damage: float
var duration: float
var tick_rate: float = 1.0
var _timer: Timer
var _remaining_ticks: int = 5
var _enemy: EnemyBase
const PUDDLE = preload("res://scenes/puddle.tscn")
func _init(enemy: EnemyBase, bleed_damage: float, duration: float):
damage = bleed_damage
_timer = Timer.new()
func apply(enemy: EnemyBase) -> void:
enemy.effects.append(self)
while _remaining_ticks > 0:
enemy.take_damage(damage, false)
_remaining_ticks -= 1
await enemy.get_tree().create_timer(1.0, false, true, false).timeout
var p = PUDDLE.instantiate()
enemy.get_parent().add_child(p)
p.global_position = enemy.global_position
enemy.effects.erase(self)
static func _is_bleeding(enemy: EnemyBase) -> bool:
return false

View File

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

View File

@@ -17,7 +17,8 @@ var modifiers: Array[EnemyMod] = []
@onready var collision_shape_2d: CollisionShape2D = $CollisionShape2D
@onready var shape_cast_2d: ShapeCast2D = $ShapeCast2D
@onready var sprite_2d: Sprite2D = $Sprite2D
@onready var label: Label = $Label
@onready var label: Label = $HBoxContainer/Label
@onready var effect_container: HBoxContainer = $HBoxContainer/EffectContainer
var player: Player
var enemy_name: String
@@ -26,7 +27,11 @@ var god_mode: bool = false
var is_dead: bool = false
var health: float
var effects: Array[EnemyEffectBase]
var _path_update_timer: float = 0.0
var _compute_cache: KeyedCache = KeyedCache.new()
var _effects_visible = []
func _ready() -> void:
@@ -63,6 +68,16 @@ func _gen_name() -> String:
return "Unnamed enemy"
func _process(delta: float) -> void:
for effect in effects:
if effect in _effects_visible:
continue
var effect_sprite = Sprite2D.new()
effect_sprite.texture = preload("res://assets/sprites/small_bleed_icon.png")
effect_container.add_child(effect_sprite)
_effects_visible.append(effect)
func _physics_process(delta: float) -> void:
if not target:
return
@@ -177,17 +192,19 @@ func _on_animation_player_animation_finished(anim_name: StringName) -> void:
func get_calculated(key: String) -> Variant:
# set max move speed to players move speed
if key == "move_speed":
return clampf(
EnemyMod.get_calculated(self, key),
0,
player.player_stats.get_final("move_speed", player.modifiers)
)
return EnemyMod.get_calculated(self, key)
var compute_func = func():
if key == "move_speed":
return clampf(
EnemyMod.get_calculated(self, key),
0,
player.player_stats.get_final("move_speed", player.modifiers)
)
return EnemyMod.get_calculated(self, key)
return _compute_cache.get_or_compute(key, compute_func)
func has_property(key: String) -> bool:
for prop in get_property_list():
if prop.name == key:
return true
return false
var cache_key = "prop_%s" % key
var compute_func = func(): return get(key) != null
return _compute_cache.get_or_compute(key, compute_func)

View File

@@ -72,7 +72,7 @@ texture = SubResource("PlaceholderTexture2D_pkqou")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_satqt")
[node name="TargetCast" type="RayCast2D" parent="." groups=["damagable", "enemy"]]
[node name="TargetCast" type="RayCast2D" parent="."]
enabled = false
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
@@ -90,20 +90,25 @@ wait_time = 0.5
shape = SubResource("CircleShape2D_pkqou")
max_results = 2
[node name="Label" type="Label" parent="."]
anchors_preset = 7
[node name="HBoxContainer" type="HBoxContainer" parent="."]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 1.0
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -85.5
offset_top = 5.0
offset_right = 85.5
offset_bottom = 28.0
anchor_bottom = 0.5
offset_left = -53.0
offset_right = 53.0
offset_bottom = 40.0
grow_horizontal = 2
grow_vertical = 0
grow_vertical = 2
[node name="EffectContainer" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_vertical = 4
[node name="Label" type="Label" parent="HBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 9
text = "Unnamed the Adjective"
horizontal_alignment = 1
[connection signal="animation_finished" from="AnimationPlayer" to="." method="_on_animation_player_animation_finished"]

View File

@@ -20,16 +20,16 @@ script = ExtResource("1_jyhfs")
zoom = Vector2(2, 2)
process_callback = 0
[node name="Player" parent="." node_paths=PackedStringArray("camera", "main_ui") instance=ExtResource("2_0wfyh")]
position = Vector2(1057, 798)
camera = NodePath("../MainCamera")
main_ui = NodePath("../MainUI")
[node name="EnemyManager" parent="." node_paths=PackedStringArray("target", "camera") instance=ExtResource("5_tbgi4")]
spawn_rate = 1.5
target = NodePath("../Player")
camera = NodePath("../MainCamera")
[node name="Player" parent="." node_paths=PackedStringArray("camera", "main_ui") instance=ExtResource("2_0wfyh")]
position = Vector2(1057, 798)
camera = NodePath("../MainCamera")
main_ui = NodePath("../MainUI")
[node name="PickupMagnet" parent="." instance=ExtResource("6_tefeu")]
position = Vector2(1697, 414)

19
scenes/main_menu.gd Normal file
View File

@@ -0,0 +1,19 @@
extends Control
const MAIN = preload("res://scenes/main.tscn")
func _ready() -> void:
pass
func _on_new_game_button_pressed() -> void:
get_tree().change_scene_to_packed(MAIN)
func _on_options_button_pressed() -> void:
pass # Replace with function body.
func _on_exit_game_button_pressed() -> void:
get_tree().quit()

1
scenes/main_menu.gd.uid Normal file
View File

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

69
scenes/main_menu.tscn Normal file
View File

@@ -0,0 +1,69 @@
[gd_scene load_steps=4 format=3 uid="uid://cynet50emve6c"]
[ext_resource type="Script" uid="uid://sd158y3mdmkt" path="res://scenes/main_menu.gd" id="1_l6cm7"]
[sub_resource type="Gradient" id="Gradient_vue75"]
colors = PackedColorArray(0.252028, 0.252028, 0.252028, 1, 0.25098, 0.25098, 0.25098, 1)
[sub_resource type="GradientTexture1D" id="GradientTexture1D_l6cm7"]
gradient = SubResource("Gradient_vue75")
[node name="MainMenu" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_l6cm7")
[node name="Background" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = SubResource("GradientTexture1D_l6cm7")
expand_mode = 2
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -20.0
offset_right = 20.0
offset_bottom = 20.0
grow_horizontal = 2
grow_vertical = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="NewGameButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "New game"
[node name="OptionsButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Options"
[node name="ExitGameButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Exit game"
[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/NewGameButton" to="." method="_on_new_game_button_pressed"]
[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/OptionsButton" to="." method="_on_options_button_pressed"]
[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/ExitGameButton" to="." method="_on_exit_game_button_pressed"]

View File

@@ -13,6 +13,8 @@ const ENEMY_BAT = preload("res://scenes/enemies/enemy_bat.tscn")
const ENEMY_SLIME_SMALL = preload("res://scenes/enemies/enemy_slime_small.tscn")
const SLIME_COLOR_VARIATIONS: Array[Color] = [Color.CHARTREUSE, Color.FUCHSIA, Color.DARK_ORANGE]
var _elapsed_time: float = 0.0
func _ready() -> void:
timer.wait_time = 1 / spawn_rate
@@ -21,7 +23,12 @@ func _ready() -> void:
GlobalConst.sig_set_spawn_rate.connect(_on_set_spawn_rate)
func _physics_process(delta: float) -> void:
_elapsed_time += delta
func _on_timer_timeout() -> void:
_on_set_spawn_rate(1.0 + (_elapsed_time / 60.0) ** 2)
var enemies = get_tree().get_nodes_in_group(GlobalConst.GROUP_ENEMY)
GlobalConst.sig_debug_stats_set.emit("enemy_count", "%s" % len(enemies))
var next_enemy: PackedScene
@@ -105,4 +112,6 @@ func _on_stop_spawning(val: bool):
func _on_set_spawn_rate(val: float):
timer.wait_time = 1 / val
timer.stop()
timer.wait_time = 1.0 / val
timer.start()

View File

@@ -29,11 +29,6 @@ func _ready() -> void:
if not weapon:
weapon = WEAPON_SWORD.instantiate()
add_child(weapon)
var mod = WeaponModBase.new()
mod.mod_property = "attack_damage"
mod.mod_value = 10.0
mod.mod_type = WeaponModBase.ModType.ADDITIVE
weapon.add_mod(mod)
func _physics_process(delta: float) -> void:

22
scenes/puddle.gd Normal file
View File

@@ -0,0 +1,22 @@
class_name Puddle
extends Node2D
@export var color: Color = Color.CRIMSON
@onready var base: Sprite2D = $Base
func _ready() -> void:
var player: Player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER)
var shader = preload("res://assets/shaders/base_color_tint.gdshader")
var shader_material: ShaderMaterial
shader_material = ShaderMaterial.new()
shader_material.set_shader_parameter("base_color", color)
shader_material.shader = shader
base.material = shader_material
match randi() % 4:
1:
rotation_degrees = 90
2:
rotation_degrees = 180
3:
rotation_degrees = -90

1
scenes/puddle.gd.uid Normal file
View File

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

34
scenes/puddle.tscn Normal file
View File

@@ -0,0 +1,34 @@
[gd_scene load_steps=8 format=3 uid="uid://dcka1mmlgeoj7"]
[ext_resource type="Shader" uid="uid://cf48pgfl308o3" path="res://assets/shaders/base_color_tint.gdshader" id="1_qlq6d"]
[ext_resource type="Script" uid="uid://cds5aqq2mqmpe" path="res://scenes/puddle.gd" id="1_s8xbh"]
[ext_resource type="Texture2D" uid="uid://c5t4it4if0s6g" path="res://assets/sprites/puddle_1.png" id="2_s8xbh"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_i64cl"]
shader = ExtResource("1_qlq6d")
shader_parameter/base_color = Color(1, 0.2, 0.2, 1)
[sub_resource type="AtlasTexture" id="AtlasTexture_h42jt"]
atlas = ExtResource("2_s8xbh")
region = Rect2(0, 0, 16, 13)
[sub_resource type="AtlasTexture" id="AtlasTexture_16dvh"]
atlas = ExtResource("2_s8xbh")
region = Rect2(32, 0, 16, 13)
[sub_resource type="AtlasTexture" id="AtlasTexture_5nuq4"]
atlas = ExtResource("2_s8xbh")
region = Rect2(16, 0, 16, 13)
[node name="Puddle" type="Node2D"]
script = ExtResource("1_s8xbh")
[node name="Base" type="Sprite2D" parent="."]
material = SubResource("ShaderMaterial_i64cl")
texture = SubResource("AtlasTexture_h42jt")
[node name="Highlights" type="Sprite2D" parent="."]
texture = SubResource("AtlasTexture_16dvh")
[node name="Shading" type="Sprite2D" parent="."]
texture = SubResource("AtlasTexture_5nuq4")

View File

@@ -0,0 +1,17 @@
class_name KeyedCache
extends Resource
var cache = {}
func get_or_compute(key: String, compute_func: Callable):
if key in cache:
return cache["key"]
var value = compute_func.call()
cache["key"] = value
return value
func invalidate_key(key: String):
cache.erase(key)

View File

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

View File

@@ -4,6 +4,8 @@ extends Node2D
enum WeaponTag {
CAN_RETURN,
CAN_BLEED,
CAN_CHAIN,
CAN_FORK,
}
@export var attack_cd: float
@@ -12,11 +14,21 @@ enum WeaponTag {
@export var attack_duration: float
@export var attack_range: float
@export var attack_crit_chance: float = 0.05
@export var return_chance: float = 0.0
@export var bleed_chance: float = 0.0
@export var bleed_duration: float = 5.0
@export var chain_chance: float = 0.0
@export var tags: Array[String] = []
@export var modifiers: Array[WeaponModBase] = []
@onready var active_cd_timer: Timer = $ActiveCDTimer
var _player: Player
func _ready() -> void:
_player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER)
func _on_attack_cd_timer_timeout() -> void:
do_attack()
@@ -76,3 +88,22 @@ func has_property(key: String) -> bool:
if prop.name == key:
return true
return false
func did_crit() -> bool:
var weapon_crit = get_calculated("attack_crit_chance")
var player_crit = _player.player_stats.get_final("crit_chance", _player.modifiers)
return randf() >= 1 - weapon_crit + player_crit
func did_bleed() -> bool:
return randf() >= 1 - bleed_chance
func base_damage() -> Array[Variant]:
var damage = get_calculated("attack_damage")
var is_crit := did_crit()
if is_crit:
damage *= _player.player_stats.get_final("crit_multiplier", _player.modifiers)
return [damage, is_crit]

View File

@@ -8,13 +8,12 @@ const WEAPON_SWORD_PROJECTILE = preload("res://scenes/weapons/weapon_sword_proje
signal projectile_hit(projectile: WeaponSwordProjectile, enemy: EnemyBase)
var _player: Player
func _ready() -> void:
bleed_chance = 0.2
targeting_range_shape.shape.radius = attack_range
projectile_hit.connect(_on_projectile_hit)
_player = get_tree().get_first_node_in_group(GlobalConst.GROUP_PLAYER)
super._ready()
func do_attack() -> void:
@@ -44,13 +43,13 @@ func _do_active() -> void:
func deal_damage(enemy: EnemyBase, damage_mult: float):
var weapon_crit = get_calculated("attack_crit_chance")
var player_crit = _player.player_stats.get_final("crit_chance", _player.modifiers)
var damage = get_calculated("attack_damage")
var is_crit = randf() >= 1 - weapon_crit + player_crit
if is_crit:
damage *= _player.player_stats.get_final("crit_multiplier", _player.modifiers)
enemy.take_damage(damage * damage_mult, is_crit)
var damage_and_crit = base_damage()
# TODO: Fix crit value
enemy.take_damage(damage_and_crit[0], damage_and_crit[1])
if did_bleed():
var bleed = EnemyEffectBleed.new(enemy, damage_and_crit[0], bleed_duration)
bleed.apply(enemy)
func _on_projectile_hit(projectile: WeaponSwordProjectile, enemy: EnemyBase, damage_mult: float):

View File

@@ -5,6 +5,7 @@ extends Node2D
@export var range: float = 200.0
@export var target: Node2D
@export var damage_mult: float = 1.0
@export var bleed_chance: float = 0.2
@export var on_hit_sig: Signal
var _direction: Vector2