diff --git a/Cirno.sln.DotSettings.user b/Cirno.sln.DotSettings.user
index 30725965..d534dd91 100644
--- a/Cirno.sln.DotSettings.user
+++ b/Cirno.sln.DotSettings.user
@@ -1,5 +1,7 @@
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
\ No newline at end of file
diff --git a/Resources/BossPhases/TestBoss1.tres b/Resources/BossPhases/TestBoss1.tres
new file mode 100644
index 00000000..df11424d
--- /dev/null
+++ b/Resources/BossPhases/TestBoss1.tres
@@ -0,0 +1,43 @@
+[gd_resource type="Resource" script_class="BossPhase" load_steps=9 format=3 uid="uid://ks6fypeil6gk"]
+
+[ext_resource type="Script" path="res://Scripts/Resources/BossPhase.cs" id="1_0cgch"]
+[ext_resource type="Script" path="res://Scripts/Resources/SimpleMovementPattern.cs" id="1_xksf5"]
+[ext_resource type="Resource" uid="uid://csudslb5tliw4" path="res://Resources/Patterns/rumia_ns_1.tres" id="2_3486e"]
+[ext_resource type="Resource" uid="uid://du2kuv125vbrx" path="res://Resources/Patterns/rumia_ns_2.tres" id="3_mwcf3"]
+
+[sub_resource type="Resource" id="Resource_acaax"]
+script = ExtResource("1_xksf5")
+relativeTargetPosition = Vector2(-100, 20)
+moveDuration = 4.0
+transitionType = 1
+easeType = 2
+WaitForCompletion = false
+
+[sub_resource type="Resource" id="Resource_o8win"]
+script = ExtResource("1_xksf5")
+relativeTargetPosition = Vector2(0, 0)
+moveDuration = 4.0
+transitionType = 1
+easeType = 2
+WaitForCompletion = false
+
+[sub_resource type="Resource" id="Resource_k77ig"]
+script = ExtResource("1_xksf5")
+relativeTargetPosition = Vector2(-100, 20)
+moveDuration = 4.0
+transitionType = 1
+easeType = 2
+WaitForCompletion = false
+
+[sub_resource type="Resource" id="Resource_5ocg5"]
+script = ExtResource("1_xksf5")
+relativeTargetPosition = Vector2(0, 0)
+moveDuration = 4.0
+transitionType = 1
+easeType = 2
+WaitForCompletion = false
+
+[resource]
+script = ExtResource("1_0cgch")
+Threshold = 20
+Patterns = Array[Resource]([SubResource("Resource_acaax"), ExtResource("2_3486e"), ExtResource("3_mwcf3"), SubResource("Resource_o8win"), ExtResource("2_3486e"), ExtResource("3_mwcf3"), SubResource("Resource_k77ig"), ExtResource("2_3486e"), ExtResource("3_mwcf3"), SubResource("Resource_5ocg5"), ExtResource("2_3486e"), ExtResource("3_mwcf3")])
diff --git a/Resources/Patterns/rumia_ns_1.tres b/Resources/Patterns/rumia_ns_1.tres
new file mode 100644
index 00000000..8ec43e54
--- /dev/null
+++ b/Resources/Patterns/rumia_ns_1.tres
@@ -0,0 +1,21 @@
+[gd_resource type="Resource" script_class="TargetedPattern" load_steps=5 format=3 uid="uid://csudslb5tliw4"]
+
+[ext_resource type="PackedScene" uid="uid://by2rk6gx67f7e" path="res://Scenes/Weapons/Bullets/enemyBullet_yellow.tscn" id="1_pocps"]
+[ext_resource type="Script" path="res://Scripts/Resources/DecreasingSpeedModifier.cs" id="2_wxqq0"]
+[ext_resource type="Script" path="res://Scripts/AttackPatterns/TargetedPattern.cs" id="3_ht3k1"]
+
+[sub_resource type="Resource" id="Resource_jeq72"]
+script = ExtResource("2_wxqq0")
+decreaseRate = 4.0
+
+[resource]
+script = ExtResource("3_ht3k1")
+BulletScene = ExtResource("1_pocps")
+bulletSpeed = 50.0
+duration = 4.0
+burstInterval = 0.4
+bulletsPerShot = 10
+spread = 0.0
+owner = 2
+modifier = SubResource("Resource_jeq72")
+WaitForCompletion = true
diff --git a/Resources/Patterns/rumia_ns_2.tres b/Resources/Patterns/rumia_ns_2.tres
new file mode 100644
index 00000000..4367a6fb
--- /dev/null
+++ b/Resources/Patterns/rumia_ns_2.tres
@@ -0,0 +1,47 @@
+[gd_resource type="Resource" script_class="PatternGroup" load_steps=8 format=3 uid="uid://du2kuv125vbrx"]
+
+[ext_resource type="PackedScene" uid="uid://b0clsnefjsohc" path="res://Scenes/Weapons/Bullets/enemyBullet_mid.tscn" id="1_tt36x"]
+[ext_resource type="Script" path="res://Scripts/AttackPatterns/SpiralPattern.cs" id="2_ee42k"]
+[ext_resource type="PackedScene" uid="uid://dohakkayqj4w2" path="res://Scenes/Weapons/Bullets/enemyBullet_green.tscn" id="3_gr7a3"]
+[ext_resource type="Script" path="res://Scripts/Resources/PatternGroup.cs" id="3_jvysx"]
+
+[sub_resource type="Resource" id="Resource_ne4q3"]
+script = ExtResource("2_ee42k")
+BulletScene = ExtResource("1_tt36x")
+bulletSpeed = 20.0
+bulletCount = 10
+rotationSpeed = 120.0
+duration = 4.0
+burstInterval = 2.0
+spread = 360.0
+owner = 2
+WaitForCompletion = true
+
+[sub_resource type="Resource" id="Resource_kohuh"]
+script = ExtResource("2_ee42k")
+BulletScene = ExtResource("3_gr7a3")
+bulletSpeed = 20.0
+bulletCount = 16
+rotationSpeed = 120.0
+duration = 4.0
+burstInterval = 2.0
+spread = 360.0
+owner = 2
+WaitForCompletion = true
+
+[sub_resource type="Resource" id="Resource_depjj"]
+script = ExtResource("2_ee42k")
+BulletScene = ExtResource("1_tt36x")
+bulletSpeed = 30.0
+bulletCount = 10
+rotationSpeed = 120.0
+duration = 4.0
+burstInterval = 2.0
+spread = 360.0
+owner = 2
+WaitForCompletion = true
+
+[resource]
+script = ExtResource("3_jvysx")
+patterns = Array[Resource]([SubResource("Resource_ne4q3"), SubResource("Resource_kohuh"), SubResource("Resource_depjj")])
+WaitForCompletion = true
diff --git a/Scenes/Actors/Rumia.tscn b/Scenes/Actors/Rumia.tscn
new file mode 100644
index 00000000..7663992d
--- /dev/null
+++ b/Scenes/Actors/Rumia.tscn
@@ -0,0 +1,85 @@
+[gd_scene load_steps=12 format=3 uid="uid://d1rlw6ddpmrn8"]
+
+[ext_resource type="Script" path="res://Scripts/Actors/Boss.cs" id="1_na4uq"]
+[ext_resource type="Resource" uid="uid://ks6fypeil6gk" path="res://Resources/BossPhases/TestBoss1.tres" id="2_1rhf6"]
+[ext_resource type="Texture2D" uid="uid://bcqgke6dthlrj" path="res://Sprites/Actors/Rumia.png" id="2_7k5gp"]
+[ext_resource type="Script" path="res://Scripts/Components/ProximityPlayerDetection.cs" id="3_gka5j"]
+[ext_resource type="PackedScene" uid="uid://crry0rgk7a8sm" path="res://Scenes/Weapons/BaseWeapon.tscn" id="4_xc6nm"]
+[ext_resource type="PackedScene" uid="uid://cuixq5ex0j40h" path="res://Scenes/enemyBullet.tscn" id="5_g1p0m"]
+[ext_resource type="Script" path="res://Scripts/Components/BulletSpawner.cs" id="7_2obh7"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_8gtts"]
+radius = 7.0
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_cacb5"]
+radius = 4.0
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_v711r"]
+radius = 85.0529
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_m1rsg"]
+size = Vector2(8, 12)
+
+[node name="Rumia" type="CharacterBody2D" groups=["Destroyable"]]
+collision_layer = 16
+collision_mask = 9
+script = ExtResource("1_na4uq")
+Phases = Array[Resource]([ExtResource("2_1rhf6")])
+metadata/_edit_group_ = true
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("2_7k5gp")
+
+[node name="Damage_HitBox" type="CollisionShape2D" parent="."]
+visible = false
+shape = SubResource("CircleShape2D_8gtts")
+
+[node name="RigidBody2D" type="RigidBody2D" parent="."]
+collision_layer = 16
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="RigidBody2D"]
+visible = false
+position = Vector2(0, 5)
+shape = SubResource("CircleShape2D_cacb5")
+
+[node name="PlayerDetection" type="Area2D" parent="."]
+visible = false
+collision_layer = 16
+collision_mask = 2
+script = ExtResource("3_gka5j")
+
+[node name="PlayerDetectionArea" type="CollisionShape2D" parent="PlayerDetection"]
+shape = SubResource("CircleShape2D_v711r")
+
+[node name="ShootTimer" type="Timer" parent="."]
+wait_time = 0.4
+one_shot = true
+
+[node name="Weapon" parent="." instance=ExtResource("4_xc6nm")]
+BulletScene = ExtResource("5_g1p0m")
+BulletCapacity = 4
+BulletSpeed = 50.0
+
+[node name="NavigationAgent2D" type="NavigationAgent2D" parent="."]
+target_desired_distance = 64.0
+path_max_distance = 800.0
+path_postprocessing = 1
+avoidance_enabled = true
+debug_enabled = true
+debug_path_custom_color = Color(1, 0, 0, 1)
+
+[node name="DamageHitbox" type="Area2D" parent="."]
+collision_layer = 16
+collision_mask = 9
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageHitbox"]
+shape = SubResource("RectangleShape2D_m1rsg")
+
+[node name="BulletSpawner" type="Node2D" parent="."]
+script = ExtResource("7_2obh7")
+BulletScene = ExtResource("5_g1p0m")
+
+[connection signal="area_entered" from="PlayerDetection" to="PlayerDetection" method="_on_area_entered"]
+[connection signal="area_exited" from="PlayerDetection" to="." method="_on_area_exited"]
+[connection signal="velocity_computed" from="NavigationAgent2D" to="." method="_on_navigation_agent_2d_velocity_computed"]
+[connection signal="area_entered" from="DamageHitbox" to="." method="_on_damage_hitbox_area_entered"]
diff --git a/Scenes/Maps/BossTestArena.tscn b/Scenes/Maps/BossTestArena.tscn
index 746b37af..794ada79 100644
--- a/Scenes/Maps/BossTestArena.tscn
+++ b/Scenes/Maps/BossTestArena.tscn
@@ -1,6 +1,17 @@
-[gd_scene load_steps=2 format=3 uid="uid://bu5fvatj2j08j"]
+[gd_scene load_steps=4 format=3 uid="uid://bu5fvatj2j08j"]
[ext_resource type="Script" path="res://Scripts/GameManager.cs" id="1_paetl"]
+[ext_resource type="PackedScene" uid="uid://d1rlw6ddpmrn8" path="res://Scenes/Actors/Rumia.tscn" id="2_47q21"]
+[ext_resource type="PackedScene" uid="uid://bghghp5ep4w2j" path="res://Scenes/player.tscn" id="2_f17ts"]
-[node name="BossTestArena" type="Node2D"]
+[node name="GameScene" type="Node2D" node_paths=PackedStringArray("PlayerSpawnMarker")]
script = ExtResource("1_paetl")
+PlayerTemplate = ExtResource("2_f17ts")
+PlayerSpawnMarker = NodePath("PlayerStartPosition")
+
+[node name="Rumia" parent="." instance=ExtResource("2_47q21")]
+position = Vector2(163, 37)
+Health = 40.0
+
+[node name="PlayerStartPosition" type="Marker2D" parent="."]
+position = Vector2(178, 115)
diff --git a/Scenes/Weapons/Bullets/enemyBullet_green.tscn b/Scenes/Weapons/Bullets/enemyBullet_green.tscn
new file mode 100644
index 00000000..05641c8c
--- /dev/null
+++ b/Scenes/Weapons/Bullets/enemyBullet_green.tscn
@@ -0,0 +1,30 @@
+[gd_scene load_steps=4 format=3 uid="uid://dohakkayqj4w2"]
+
+[ext_resource type="Script" path="res://Scripts/Bullet.cs" id="1_vg1kg"]
+[ext_resource type="Texture2D" uid="uid://dmwi86k7hr8sl" path="res://Sprites/Bullets/small_bullet_green.png" id="2_divgu"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_jxptd"]
+radius = 2.23607
+
+[node name="Bullet" type="Area2D" groups=["bullets"]]
+collision_layer = 128
+collision_mask = 71
+script = ExtResource("1_vg1kg")
+Speed = 200.0
+Owner = 2
+metadata/_edit_group_ = true
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("2_divgu")
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource("CircleShape2D_jxptd")
+
+[node name="Node2D" type="Node2D" parent="."]
+editor_description = "Player Bullet"
+
+[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."]
+
+[connection signal="area_entered" from="." to="." method="_on_area_entered"]
+[connection signal="body_entered" from="." to="." method="_on_body_entered"]
+[connection signal="screen_exited" from="VisibleOnScreenNotifier2D" to="." method="_on_visible_on_screen_notifier_2d_screen_exited"]
diff --git a/Scenes/Weapons/Bullets/enemyBullet_mid.tscn b/Scenes/Weapons/Bullets/enemyBullet_mid.tscn
new file mode 100644
index 00000000..949e888a
--- /dev/null
+++ b/Scenes/Weapons/Bullets/enemyBullet_mid.tscn
@@ -0,0 +1,30 @@
+[gd_scene load_steps=4 format=3 uid="uid://b0clsnefjsohc"]
+
+[ext_resource type="Script" path="res://Scripts/Bullet.cs" id="1_w5w28"]
+[ext_resource type="Texture2D" uid="uid://c4ijhpgbwmbr8" path="res://Sprites/Bullets/mid_bullet.png" id="2_5gscp"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_jxptd"]
+radius = 4.0
+
+[node name="Bullet" type="Area2D" groups=["bullets"]]
+collision_layer = 128
+collision_mask = 71
+script = ExtResource("1_w5w28")
+Speed = 200.0
+Owner = 2
+metadata/_edit_group_ = true
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("2_5gscp")
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource("CircleShape2D_jxptd")
+
+[node name="Node2D" type="Node2D" parent="."]
+editor_description = "Player Bullet"
+
+[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."]
+
+[connection signal="area_entered" from="." to="." method="_on_area_entered"]
+[connection signal="body_entered" from="." to="." method="_on_body_entered"]
+[connection signal="screen_exited" from="VisibleOnScreenNotifier2D" to="." method="_on_visible_on_screen_notifier_2d_screen_exited"]
diff --git a/Scenes/Weapons/Bullets/enemyBullet_yellow.tscn b/Scenes/Weapons/Bullets/enemyBullet_yellow.tscn
new file mode 100644
index 00000000..25347832
--- /dev/null
+++ b/Scenes/Weapons/Bullets/enemyBullet_yellow.tscn
@@ -0,0 +1,27 @@
+[gd_scene load_steps=4 format=3 uid="uid://by2rk6gx67f7e"]
+
+[ext_resource type="Script" path="res://Scripts/Bullet.cs" id="1_hd5v3"]
+[ext_resource type="Texture2D" uid="uid://s5argltaljmq" path="res://Sprites/Bullets/small_bullet_yellow.png" id="2_c3vqu"]
+
+[sub_resource type="CircleShape2D" id="CircleShape2D_jxptd"]
+radius = 2.23607
+
+[node name="Bullet" type="Area2D" groups=["bullets"]]
+collision_layer = 128
+collision_mask = 71
+script = ExtResource("1_hd5v3")
+Speed = 200.0
+Owner = 2
+metadata/_edit_group_ = true
+
+[node name="Sprite2D" type="Sprite2D" parent="."]
+texture = ExtResource("2_c3vqu")
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource("CircleShape2D_jxptd")
+
+[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."]
+
+[connection signal="area_entered" from="." to="." method="_on_area_entered"]
+[connection signal="body_entered" from="." to="." method="_on_body_entered"]
+[connection signal="screen_exited" from="VisibleOnScreenNotifier2D" to="." method="_on_visible_on_screen_notifier_2d_screen_exited"]
diff --git a/Scripts/Actors/Boss.cs b/Scripts/Actors/Boss.cs
new file mode 100644
index 00000000..f0f31d71
--- /dev/null
+++ b/Scripts/Actors/Boss.cs
@@ -0,0 +1,54 @@
+using Cirno.Scripts.Resources;
+using Godot;
+using Godot.Collections;
+
+namespace Cirno.Scripts.Actors;
+
+public partial class Boss : Enemy
+{
+ [Export] private Array Phases;
+ private int currentPhaseIndex = 0;
+
+ private GameManager _gameManager;
+
+ private Vector2 _homePosition;
+ public Vector2 HomePosition => _homePosition;
+
+ public GameManager GameManager => _gameManager;
+
+ private BossPhase CurrentPhase => Phases[currentPhaseIndex];
+
+ public override void _Ready()
+ {
+ base._Ready();
+ _gameManager = GetNode("/root/GameScene");
+
+ _homePosition = this.GlobalPosition;
+
+ StartPhase(CurrentPhase);
+ }
+
+ public override void _Process(double delta)
+ {
+ base._Process(delta);
+
+ CurrentPhase.UpdatePhase(delta);
+ if (_currentHealth <= CurrentPhase.Threshold && currentPhaseIndex + 1 < Phases.Count)
+ {
+ currentPhaseIndex++;
+ StartPhase(CurrentPhase);
+ }
+
+ }
+
+ private void StartPhase(BossPhase phase)
+ {
+ phase.Start(this);
+ }
+
+ public void TakeDamage(int amount)
+ {
+ _currentHealth -= amount;
+ }
+
+}
\ No newline at end of file
diff --git a/Scripts/AttackPatterns/MovementPattern.cs b/Scripts/AttackPatterns/MovementPattern.cs
new file mode 100644
index 00000000..9f75f601
--- /dev/null
+++ b/Scripts/AttackPatterns/MovementPattern.cs
@@ -0,0 +1,54 @@
+using Cirno.Scripts.Actors;
+using Cirno.Scripts.Resources;
+using Godot;
+
+namespace Cirno.Scripts.AttackPatterns;
+
+[GlobalClass]
+public partial class MovementPattern : AttackPattern
+{
+ [Export] private Vector2 relativeTargetPosition;
+ [Export] private float moveDuration = 2f;
+ [Export] private Tween.TransitionType transitionType = Tween.TransitionType.Linear;
+ [Export] private Tween.EaseType easeType = Tween.EaseType.InOut;
+ [Export] private AttackPattern shootingPattern;
+
+
+ private Tween tween;
+ private bool isComplete = false;
+
+ public override void Start(Boss boss)
+ {
+ Boss = boss;
+ tween = boss.CreateTween();
+ isComplete = false;
+
+ Vector2 targetPosition = (Boss?.HomePosition ?? boss.GlobalPosition) + this.relativeTargetPosition;
+
+
+ tween.TweenProperty(Boss, "position", targetPosition, moveDuration)
+ .SetTrans(transitionType)
+ .SetEase(easeType)
+ .Finished += () => isComplete = true;
+
+ if (shootingPattern != null && !WaitForCompletion)
+ {
+ shootingPattern.Start(boss);
+ }
+ }
+
+ public override void UpdatePattern(double delta)
+ {
+ if (shootingPattern != null && !WaitForCompletion)
+ {
+ shootingPattern.UpdatePattern(delta);
+ }
+ }
+
+ public override bool IsComplete()
+ {
+ if (WaitForCompletion && shootingPattern != null)
+ return isComplete && shootingPattern.IsComplete();
+ return isComplete;
+ }
+}
\ No newline at end of file
diff --git a/Scripts/AttackPatterns/PatternTest.cs b/Scripts/AttackPatterns/PatternTest.cs
new file mode 100644
index 00000000..ba5295a9
--- /dev/null
+++ b/Scripts/AttackPatterns/PatternTest.cs
@@ -0,0 +1,45 @@
+using Cirno.Scripts.Actors;
+using Cirno.Scripts.Components;
+using Cirno.Scripts.Resources;
+using Godot;
+
+namespace Cirno.Scripts.AttackPatterns;
+
+[GlobalClass]
+public partial class PatternTest : AttackPattern
+{
+ [Export] public PackedScene BulletScene;
+ [Export] private float bulletSpeed = 5f;
+ [Export] private int bulletCount = 12;
+ [Export] private float duration = 3f;
+ [Export] private float burstInterval = 0.5f;
+ [Export] private BulletOwner owner = BulletOwner.Enemy;
+
+ private double timer;
+ private double burstTimer;
+ private BulletSpawner spawner;
+
+ public override void Start(Boss boss)
+ {
+ Boss = boss;
+ timer = 0;
+ burstTimer = 0;
+ spawner = boss.GetNode("BulletSpawner");
+ }
+
+ public override void UpdatePattern(double delta)
+ {
+ timer += delta;
+ burstTimer += delta;
+ if (timer < duration && burstTimer >= burstInterval)
+ {
+ spawner.SpawnBullet(Boss.GlobalPosition, Vector2.Right, bulletSpeed, owner, bulletCount, bulletScene: BulletScene);
+ burstTimer = 0;
+ }
+ }
+
+ public override bool IsComplete()
+ {
+ return timer >= duration;
+ }
+}
\ No newline at end of file
diff --git a/Scripts/AttackPatterns/SpiralPattern.cs b/Scripts/AttackPatterns/SpiralPattern.cs
new file mode 100644
index 00000000..0cc4bf71
--- /dev/null
+++ b/Scripts/AttackPatterns/SpiralPattern.cs
@@ -0,0 +1,47 @@
+using Cirno.Scripts.Actors;
+using Cirno.Scripts.Components;
+using Cirno.Scripts.Resources;
+using Godot;
+
+namespace Cirno.Scripts.AttackPatterns;
+
+[GlobalClass]
+public partial class SpiralPattern : AttackPattern
+{
+ [Export] public PackedScene BulletScene;
+ [Export] private float bulletSpeed = 5f;
+ [Export] private int bulletCount = 16;
+ [Export] private float rotationSpeed = 90f;
+ [Export] private float duration = 5f;
+ [Export] private float burstInterval = 0.5f;
+ [Export] private float spread = 360f;
+ [Export] private BulletOwner owner = BulletOwner.Enemy;
+
+ private double timer;
+ private double burstTimer;
+ private BulletSpawner spawner;
+
+ public override void Start(Boss boss)
+ {
+ Boss = boss;
+ timer = 0;
+ burstTimer = burstInterval; // start immediately
+ spawner = boss.GetNode("BulletSpawner");
+ }
+
+ public override void UpdatePattern(double delta)
+ {
+ timer += delta;
+ burstTimer += delta;
+ if (timer < duration && burstTimer >= burstInterval)
+ {
+ spawner.SpawnSpiralPattern(Boss.GlobalPosition, bulletSpeed, owner, bulletCount, rotationSpeed, timer, spread, BulletScene);
+ burstTimer = 0;
+ }
+ }
+
+ public override bool IsComplete()
+ {
+ return timer >= duration;
+ }
+}
diff --git a/Scripts/AttackPatterns/TargetedPattern.cs b/Scripts/AttackPatterns/TargetedPattern.cs
new file mode 100644
index 00000000..195a1b4a
--- /dev/null
+++ b/Scripts/AttackPatterns/TargetedPattern.cs
@@ -0,0 +1,54 @@
+using Cirno.Scripts.Actors;
+using Cirno.Scripts.Components;
+using Cirno.Scripts.Resources;
+
+namespace Cirno.Scripts.AttackPatterns;
+
+using Godot;
+
+[GlobalClass]
+public partial class TargetedPattern : AttackPattern
+{
+ [Export] public PackedScene BulletScene;
+
+ [Export] private float bulletSpeed = 5f;
+ [Export] private float duration = 3f;
+ [Export] private float burstInterval = 0.5f;
+ [Export] private int bulletsPerShot = 1;
+ [Export] private float spread = 0f;
+ [Export] private BulletOwner owner = BulletOwner.Enemy;
+ [Export] private Resource modifier;
+
+ private double timer;
+ private double burstTimer;
+ private BulletSpawner spawner;
+ //private Node2D player;
+ private GameManager _gameManager;
+
+ public override void Start(Boss boss)
+ {
+ _gameManager = boss.GetNode("/root/GameScene");
+
+ Boss = boss;
+ timer = 0;
+ burstTimer = 0;
+ spawner = boss.GetNode("BulletSpawner");
+
+ }
+
+ public override void UpdatePattern(double delta)
+ {
+ timer += delta;
+ burstTimer += delta;
+ if (timer < duration && burstTimer >= burstInterval && _gameManager.PlayerPosition.HasValue)
+ {
+ spawner.SpawnTargetedBullet(Boss.GlobalPosition, _gameManager.PlayerPosition.Value, bulletSpeed, owner, BulletScene, bulletsPerShot, spread, modifier as IBulletModifier);
+ burstTimer = 0;
+ }
+ }
+
+ public override bool IsComplete()
+ {
+ return timer >= duration;
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Bullet.cs b/Scripts/Bullet.cs
index bcae18ea..7345eb82 100644
--- a/Scripts/Bullet.cs
+++ b/Scripts/Bullet.cs
@@ -42,7 +42,6 @@ public partial class Bullet : Area2D
//Debug.WriteLine($"Bullet Shot at direction {direction.X} {direction.Y}");
}
-
// Called every frame. 'delta' is the elapsed time since the previous frame.
public override void _Process(double delta)
{
diff --git a/Scripts/Components/BulletSpawner.cs b/Scripts/Components/BulletSpawner.cs
new file mode 100644
index 00000000..9dc11827
--- /dev/null
+++ b/Scripts/Components/BulletSpawner.cs
@@ -0,0 +1,53 @@
+using Cirno.Scripts.Resources;
+using Godot;
+
+namespace Cirno.Scripts.Components;
+
+public partial class BulletSpawner : Node2D
+{
+ [Export] public PackedScene BulletScene;
+ private GameManager _gameManager;
+
+ public override void _Ready()
+ {
+ _gameManager = GetNode("/root/GameScene");
+ }
+
+ public void SpawnBullet(Vector2 position, Vector2 direction, float speed, BulletOwner owner, int count = 1, float angleOffset = 0, float spread = 0, PackedScene bulletScene = null, IBulletModifier modifier = null)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ var bullet = this.CreateChildOf(_gameManager.BulletsContainer, bulletScene ?? BulletScene, position);
+
+ //var bullet = BulletScene.Instantiate();
+ bullet.Position = position;
+ bullet.Owner = owner;
+ //bullet.Speed = speed;
+
+ float modifiedSpeed = modifier?.ModifySpeed(speed, i) ?? speed;
+ bullet.Speed = modifiedSpeed;
+
+ Vector2 baseDirection = direction == Vector2.Zero ? Vector2.Right : direction.Normalized();
+ float baseAngle = Mathf.Atan2(baseDirection.Y, baseDirection.X);
+ //float angle = angleOffset + (360 / count) * i;
+ float angle = baseAngle + Mathf.DegToRad(angleOffset + (spread / count) * i);
+ Vector2 bulletDirection = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
+ //Vector2 bulletDirection = new Vector2(Mathf.Cos(Mathf.DegToRad(angle)), Mathf.Sin(Mathf.DegToRad(angle)));
+
+ bullet.SetDirection(bulletDirection);
+ //GetParent().AddChild(bullet);
+ }
+ }
+
+ public void SpawnTargetedBullet(Vector2 position, Vector2 target, float speed, BulletOwner owner, PackedScene bulletScene = null, int burstCount = 1, float spread = 0, IBulletModifier modifier = null)
+ {
+ Vector2 direction = (target - position).Normalized();
+ SpawnBullet(position, direction, speed, owner, burstCount, spread: spread, bulletScene: bulletScene, modifier: modifier);
+ }
+
+ public void SpawnSpiralPattern(Vector2 position, float speed, BulletOwner owner, int bulletCount, float rotationSpeed, double time, float spread, PackedScene bulletScene = null)
+ {
+ float angleOffset = (float)(rotationSpeed * time);
+ SpawnBullet(position, Vector2.Right, speed, owner, bulletCount, angleOffset, spread, bulletScene: bulletScene);
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Enemy.cs b/Scripts/Enemy.cs
index 67ea24e7..f7d0ae8b 100644
--- a/Scripts/Enemy.cs
+++ b/Scripts/Enemy.cs
@@ -8,6 +8,12 @@ using Godot.Collections;
public partial class Enemy : CharacterBody2D
{
private InteractionController _cachedPlayer;
+ public InteractionController CachedPlayer
+ {
+ get => _cachedPlayer;
+ protected set => _cachedPlayer = value;
+ }
+
private EnemyState _currentState = EnemyState.Idle;
[Export] public float Health = 4f;
@@ -20,7 +26,7 @@ public partial class Enemy : CharacterBody2D
[Export] public Weapon EquippedWeapon;
- private float _currentHealth = 0f;
+ protected float _currentHealth = 0f;
private bool _isDestroyed = false;
@@ -52,9 +58,12 @@ public partial class Enemy : CharacterBody2D
_navigationAgent = GetNodeOrNull("NavigationAgent2D");
- _alarmManager = GetNode("/root/GameScene/AlarmManager");
-
- _alarmManager.AlarmEnabled += AlarmManagerOnAlarmEnabled;
+ _alarmManager = GetNodeOrNull("/root/GameScene/AlarmManager");
+
+ if (_alarmManager != null)
+ {
+ _alarmManager.AlarmEnabled += AlarmManagerOnAlarmEnabled;
+ }
}
private void AlarmManagerOnAlarmEnabled(Vector2 location)
diff --git a/Scripts/GameManager.cs b/Scripts/GameManager.cs
index fde89439..24b66717 100644
--- a/Scripts/GameManager.cs
+++ b/Scripts/GameManager.cs
@@ -7,9 +7,13 @@ public partial class GameManager : Node2D
private Hud _hud;
private PlayerMovement _player;
+
+ public PlayerMovement Player => _player;
private Node2D _cameraTarget;
+ public Vector2? PlayerPosition => _player?.GlobalPosition ?? null;
+
[Export]
public PackedScene PlayerTemplate { get; set; }
diff --git a/Scripts/Resources/AttackPattern.cs b/Scripts/Resources/AttackPattern.cs
new file mode 100644
index 00000000..ad81916d
--- /dev/null
+++ b/Scripts/Resources/AttackPattern.cs
@@ -0,0 +1,13 @@
+using Cirno.Scripts.Actors;
+using Godot;
+
+namespace Cirno.Scripts.Resources;
+
+public abstract partial class AttackPattern : Resource
+{
+ public Boss Boss;
+ [Export] public bool WaitForCompletion = true;
+ public abstract void Start(Boss boss);
+ public abstract void UpdatePattern(double delta);
+ public abstract bool IsComplete();
+}
\ No newline at end of file
diff --git a/Scripts/Resources/BossPhase.cs b/Scripts/Resources/BossPhase.cs
new file mode 100644
index 00000000..aca76e01
--- /dev/null
+++ b/Scripts/Resources/BossPhase.cs
@@ -0,0 +1,35 @@
+using Cirno.Scripts.Actors;
+using Godot;
+using Godot.Collections;
+
+namespace Cirno.Scripts.Resources;
+
+[GlobalClass]
+public partial class BossPhase : Resource
+{
+ [Export] public int Threshold;
+ [Export] public Array Patterns;
+
+ private int currentPatternIndex = 0;
+ private double patternTimer;
+
+ public void Start(Boss boss)
+ {
+ currentPatternIndex = 0;
+ Patterns[currentPatternIndex].Start(boss);
+ }
+
+ public void UpdatePhase(double delta)
+ {
+ patternTimer += delta;
+ var currentPattern = Patterns[currentPatternIndex];
+
+ currentPattern.UpdatePattern(delta);
+
+ if (!currentPattern.WaitForCompletion || currentPattern.IsComplete())
+ {
+ currentPatternIndex = (currentPatternIndex + 1) % Patterns.Count;
+ Patterns[currentPatternIndex].Start(currentPattern.Boss);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Resources/DecreasingSpeedModifier.cs b/Scripts/Resources/DecreasingSpeedModifier.cs
new file mode 100644
index 00000000..545e53ce
--- /dev/null
+++ b/Scripts/Resources/DecreasingSpeedModifier.cs
@@ -0,0 +1,14 @@
+using Godot;
+
+namespace Cirno.Scripts.Resources;
+
+[GlobalClass]
+public partial class DecreasingSpeedModifier : Resource, IBulletModifier
+{
+ [Export] private float decreaseRate = 0.1f;
+
+ public float ModifySpeed(float baseSpeed, int bulletIndex)
+ {
+ return Mathf.Max(0, baseSpeed - (decreaseRate * bulletIndex));
+ }
+}
\ No newline at end of file
diff --git a/Scripts/Resources/IBulletModifier.cs b/Scripts/Resources/IBulletModifier.cs
new file mode 100644
index 00000000..114e7c84
--- /dev/null
+++ b/Scripts/Resources/IBulletModifier.cs
@@ -0,0 +1,6 @@
+namespace Cirno.Scripts.Resources;
+
+public interface IBulletModifier
+{
+ float ModifySpeed(float baseSpeed, int bulletIndex);
+}
\ No newline at end of file
diff --git a/Scripts/Resources/PatternGroup.cs b/Scripts/Resources/PatternGroup.cs
new file mode 100644
index 00000000..077ec874
--- /dev/null
+++ b/Scripts/Resources/PatternGroup.cs
@@ -0,0 +1,42 @@
+using Godot;
+using System.Collections.Generic;
+using Cirno.Scripts.Actors;
+using Godot.Collections;
+
+namespace Cirno.Scripts.Resources;
+
+[GlobalClass]
+public partial class PatternGroup : AttackPattern
+{
+ [Export] private Array patterns;
+ private int currentPatternIndex = 0;
+
+ public override void Start(Boss boss)
+ {
+ Boss = boss;
+ currentPatternIndex = 0;
+ patterns[currentPatternIndex].Start(boss);
+ }
+
+ public override void UpdatePattern(double delta)
+ {
+ if (currentPatternIndex < patterns.Count)
+ {
+ patterns[currentPatternIndex].UpdatePattern(delta);
+
+ if (!patterns[currentPatternIndex].WaitForCompletion || patterns[currentPatternIndex].IsComplete())
+ {
+ currentPatternIndex++;
+ if (currentPatternIndex < patterns.Count)
+ {
+ patterns[currentPatternIndex].Start(Boss);
+ }
+ }
+ }
+ }
+
+ public override bool IsComplete()
+ {
+ return currentPatternIndex >= patterns.Count;
+ }
+}
diff --git a/Scripts/Resources/SimpleMovementPattern.cs b/Scripts/Resources/SimpleMovementPattern.cs
new file mode 100644
index 00000000..5179a6ca
--- /dev/null
+++ b/Scripts/Resources/SimpleMovementPattern.cs
@@ -0,0 +1,37 @@
+using Cirno.Scripts.Actors;
+using Godot;
+
+namespace Cirno.Scripts.Resources;
+
+[GlobalClass]
+public partial class SimpleMovementPattern : AttackPattern
+{
+ [Export] private Vector2 relativeTargetPosition;
+ [Export] private float moveDuration = 2f;
+ [Export] private Tween.TransitionType transitionType = Tween.TransitionType.Linear;
+ [Export] private Tween.EaseType easeType = Tween.EaseType.InOut;
+
+ private Tween tween;
+ private bool isComplete = false;
+
+ public override void Start(Boss boss)
+ {
+ Boss = boss;
+ tween = boss.CreateTween();
+ isComplete = false;
+
+ Vector2 targetPosition = (Boss?.HomePosition ?? boss.GlobalPosition) + relativeTargetPosition;
+
+ tween.TweenProperty(Boss, "position", targetPosition, moveDuration)
+ .SetTrans(transitionType)
+ .SetEase(easeType)
+ .Finished += () => isComplete = true;
+ }
+
+ public override void UpdatePattern(double delta) { }
+
+ public override bool IsComplete()
+ {
+ return isComplete;
+ }
+}
diff --git a/Scripts/Weapon.cs b/Scripts/Weapon.cs
index f8b3479d..6e78f0af 100644
--- a/Scripts/Weapon.cs
+++ b/Scripts/Weapon.cs
@@ -97,7 +97,6 @@ public partial class Weapon : Node2D
// Rotate the ShootDirection by the spread angle
Vector2 spreadDirection = ShootDirection.Rotated(Mathf.DegToRad(spreadOffset));
-
var bullet = this.CreateChildOf(_gameManager.BulletsContainer, BulletScene, _muzzle.GlobalPosition);
diff --git a/Sprites/Actors/Rumia.aseprite b/Sprites/Actors/Rumia.aseprite
new file mode 100644
index 00000000..d12d4724
--- /dev/null
+++ b/Sprites/Actors/Rumia.aseprite
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4b18da862b2c05c3f301384228fa9a26910d78c2aa7d2d27b4bf080048203ac
+size 747
diff --git a/Sprites/Actors/Rumia.png b/Sprites/Actors/Rumia.png
new file mode 100644
index 00000000..30388367
--- /dev/null
+++ b/Sprites/Actors/Rumia.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d56a902fe7d5dac26a5fd07fa7b457346dca049ee695f61cce40aa72643fe374
+size 302
diff --git a/Sprites/Actors/Rumia.png.import b/Sprites/Actors/Rumia.png.import
new file mode 100644
index 00000000..c7aee731
--- /dev/null
+++ b/Sprites/Actors/Rumia.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bcqgke6dthlrj"
+path="res://.godot/imported/Rumia.png-17dc64f86b8ab2ed278a5be8a865a79b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Sprites/Actors/Rumia.png"
+dest_files=["res://.godot/imported/Rumia.png-17dc64f86b8ab2ed278a5be8a865a79b.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
diff --git a/Sprites/Bullets/mid_bullet.aseprite b/Sprites/Bullets/mid_bullet.aseprite
new file mode 100644
index 00000000..5d03ef2a
--- /dev/null
+++ b/Sprites/Bullets/mid_bullet.aseprite
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:64927260c2b071963e7d1c78d9f562d4a0eeeb14995cd22b102cc10d9f93acf9
+size 596
diff --git a/Sprites/Bullets/mid_bullet.png b/Sprites/Bullets/mid_bullet.png
new file mode 100644
index 00000000..421e814d
--- /dev/null
+++ b/Sprites/Bullets/mid_bullet.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6a22fcb0add8362552e80cdc0e9ac8e5fd9fc5bb60988f66c8c7414ccb416aeb
+size 164
diff --git a/Sprites/Bullets/mid_bullet.png.import b/Sprites/Bullets/mid_bullet.png.import
new file mode 100644
index 00000000..ccbbd377
--- /dev/null
+++ b/Sprites/Bullets/mid_bullet.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c4ijhpgbwmbr8"
+path="res://.godot/imported/mid_bullet.png-1ac58965f8b5ef0dd6b167a05ec5fb5b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Sprites/Bullets/mid_bullet.png"
+dest_files=["res://.godot/imported/mid_bullet.png-1ac58965f8b5ef0dd6b167a05ec5fb5b.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
diff --git a/Sprites/Bullets/small_bullet_green.ase b/Sprites/Bullets/small_bullet_green.ase
new file mode 100644
index 00000000..ebf9152a
Binary files /dev/null and b/Sprites/Bullets/small_bullet_green.ase differ
diff --git a/Sprites/Bullets/small_bullet_green.png b/Sprites/Bullets/small_bullet_green.png
new file mode 100644
index 00000000..bed7476c
--- /dev/null
+++ b/Sprites/Bullets/small_bullet_green.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d1c86fb55da8caa25aab748c73097c8b18d68117cb289580d93e61e908e72c6d
+size 100
diff --git a/Sprites/Bullets/small_bullet_green.png.import b/Sprites/Bullets/small_bullet_green.png.import
new file mode 100644
index 00000000..9374b641
--- /dev/null
+++ b/Sprites/Bullets/small_bullet_green.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dmwi86k7hr8sl"
+path="res://.godot/imported/small_bullet_green.png-06ba77c665a606eaa98def679ad773e0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Sprites/Bullets/small_bullet_green.png"
+dest_files=["res://.godot/imported/small_bullet_green.png-06ba77c665a606eaa98def679ad773e0.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
diff --git a/Sprites/Bullets/small_bullet_yellow.ase b/Sprites/Bullets/small_bullet_yellow.ase
new file mode 100644
index 00000000..5b1b2fa3
Binary files /dev/null and b/Sprites/Bullets/small_bullet_yellow.ase differ
diff --git a/Sprites/Bullets/small_bullet_yellow.png b/Sprites/Bullets/small_bullet_yellow.png
new file mode 100644
index 00000000..0868634f
--- /dev/null
+++ b/Sprites/Bullets/small_bullet_yellow.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:200e2933ad737c8ba640f9ad03cd82070531073dea8821ed618b064a8e0c0154
+size 110
diff --git a/Sprites/Bullets/small_bullet_yellow.png.import b/Sprites/Bullets/small_bullet_yellow.png.import
new file mode 100644
index 00000000..3a90d65d
--- /dev/null
+++ b/Sprites/Bullets/small_bullet_yellow.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://s5argltaljmq"
+path="res://.godot/imported/small_bullet_yellow.png-41c6526899201bd1a4e75ddd93c0f76e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://Sprites/Bullets/small_bullet_yellow.png"
+dest_files=["res://.godot/imported/small_bullet_yellow.png-41c6526899201bd1a4e75ddd93c0f76e.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
diff --git a/project.godot b/project.godot
index f09111a0..8a599ab4 100644
--- a/project.godot
+++ b/project.godot
@@ -21,7 +21,7 @@ Settings/temporaryLogLifetime=5.0
[application]
config/name="Cirno"
-run/main_scene="res://Scenes/test.tscn"
+run/main_scene="res://Scenes/Maps/BossTestArena.tscn"
config/features=PackedStringArray("4.3", "C#", "GL Compatibility")
config/icon="res://icon.svg"