This commit is contained in:
Marco 2025-06-21 15:41:29 +02:00
commit ede8f2028a
34 changed files with 1418 additions and 417 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=59 format=3 uid="uid://ec4m3geediis"]
[gd_scene load_steps=60 format=3 uid="uid://ec4m3geediis"]
[ext_resource type="Script" uid="uid://cvisn0b641od4" path="res://addons/cyclops_level_builder/nodes/cyclops_block.gd" id="1_18fbr"]
[ext_resource type="Script" uid="uid://ba0tf7ihw4hpp" path="res://Scripts/Misc/CameraController3D.cs" id="1_g4gcm"]
@ -19,6 +19,7 @@
[ext_resource type="PackedScene" uid="uid://c8gtrjf2xeue7" path="res://3D/MapScenes/TestLevel.tscn" id="12_g83w3"]
[ext_resource type="Script" uid="uid://dnslcy71dgea" path="res://Scripts/Misc/CameraTarget3D.cs" id="16_e2nai"]
[ext_resource type="PackedScene" uid="uid://cupulrjeeivxm" path="res://3D/MapScenes/TestLevel2.tscn" id="18_e2nai"]
[ext_resource type="PackedScene" uid="uid://bh3vxmqflijgj" path="res://Scenes/Actors/Generic_Enemy_FSM_3D.tscn" id="20_1dvih"]
[sub_resource type="Resource" id="Resource_id3mo"]
script = ExtResource("2_kler0")
@ -483,7 +484,7 @@ hframes = 4
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.84862, 0, -4.8932)
[node name="StartPosition" type="Marker3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 21.5972, 1.57535, 17.7437)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 22.8199, 1.57535, 10.1004)
[node name="CameraTarget" type="Marker3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 21.0389, 2.33215, 3.16925)
@ -500,3 +501,6 @@ TargetPath = NodePath("../CameraTarget")
[node name="TestLevel2" parent="." instance=ExtResource("18_e2nai")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 69.0028, 0, -23.3622)
[node name="FairyGuardFsm" parent="." instance=ExtResource("20_1dvih")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 20.7257, 1.4, 18.9936)

View file

@ -0,0 +1,69 @@
[gd_resource type="Resource" script_class="EnemyResource" load_steps=17 format=3 uid="uid://ccym6mcq4fbul"]
[ext_resource type="SpriteFrames" uid="uid://ch2ll1on8im2p" path="res://Resources/Sprites/FairyGuard.tres" id="1_b2551"]
[ext_resource type="Texture2D" uid="uid://xhwfgbv0fjbr" path="res://Sprites/Actors/FairyGuard.png" id="2_c6xyh"]
[ext_resource type="Script" uid="uid://cq65aed620ijo" path="res://Scripts/Resources/Loot/LootDrop.cs" id="3_juf1x"]
[ext_resource type="Resource" uid="uid://ct1fa2huvy34n" path="res://Resources/Items/Ammo1.tres" id="4_m2lqx"]
[ext_resource type="Resource" uid="uid://dy53gia1tmkah" path="res://Resources/Items/Points_Pickup.tres" id="5_80clb"]
[ext_resource type="Resource" uid="uid://bhbufxodybsw4" path="res://Resources/Items/Shield_Pickup.tres" id="6_ili73"]
[ext_resource type="Resource" uid="uid://dodwpect0ldjf" path="res://Resources/Items/Heart_Pickup.tres" id="7_7ibiq"]
[ext_resource type="Resource" uid="uid://clr1gln7nxa1o" path="res://Resources/Items/Power_Pickup.tres" id="8_lij2i"]
[ext_resource type="Resource" uid="uid://c6ywv08e6is5o" path="res://Resources/Weapons/EnemyWeapon_Big_3D.tres" id="9_b2551"]
[ext_resource type="Script" uid="uid://cd5o0ceb50jki" path="res://Scripts/Resources/EnemyResource.cs" id="10_by7h3"]
[sub_resource type="AtlasTexture" id="AtlasTexture_n54y5"]
atlas = ExtResource("2_c6xyh")
region = Rect2(0, 0, 16, 16)
[sub_resource type="Resource" id="Resource_c8nix"]
script = ExtResource("3_juf1x")
Item = ExtResource("4_m2lqx")
Chance = 40.0
metadata/_custom_type_script = "uid://cq65aed620ijo"
[sub_resource type="Resource" id="Resource_gs2l3"]
script = ExtResource("3_juf1x")
Item = ExtResource("5_80clb")
Chance = 10.0
metadata/_custom_type_script = "uid://cq65aed620ijo"
[sub_resource type="Resource" id="Resource_sqnvg"]
script = ExtResource("3_juf1x")
Item = ExtResource("6_ili73")
Chance = 5.0
metadata/_custom_type_script = "uid://cq65aed620ijo"
[sub_resource type="Resource" id="Resource_5tyar"]
script = ExtResource("3_juf1x")
Item = ExtResource("7_7ibiq")
Chance = 5.0
metadata/_custom_type_script = "uid://cq65aed620ijo"
[sub_resource type="Resource" id="Resource_48xq6"]
script = ExtResource("3_juf1x")
Item = ExtResource("8_lij2i")
Chance = 6.0
metadata/_custom_type_script = "uid://cq65aed620ijo"
[resource]
script = ExtResource("10_by7h3")
EnemyName = &"Fairy Guard"
EnemyKey = &"FAIRY_GUARD"
PrefabPath = &"uid://bh3vxmqflijgj"
MaxHealth = 10.0
MovementSpeed = 2.0
Weapon = ExtResource("9_b2551")
LootDrops = Array[ExtResource("3_juf1x")]([SubResource("Resource_c8nix"), SubResource("Resource_gs2l3"), SubResource("Resource_sqnvg"), SubResource("Resource_5tyar"), SubResource("Resource_48xq6")])
MotivationReward = 4.0
PredictPlayer = false
PlayerDetectionRange = 4.0
ViewRange = 5.0
AlarmReactRange = 8.0
PlayerDisengageRange = 10.0
StrafeSpeed = 1.5
MaxStrafeDistance = 1.0
MinStrafeDistance = 0.2
ResponseTime = 0.5
IconSprite = SubResource("AtlasTexture_n54y5")
AnimationFrames = ExtResource("1_b2551")
metadata/_custom_type_script = "uid://cd5o0ceb50jki"

View file

@ -0,0 +1,22 @@
[gd_resource type="Resource" script_class="WeaponResource" load_steps=3 format=3 uid="uid://c6ywv08e6is5o"]
[ext_resource type="Resource" uid="uid://wbdspte0ch33" path="res://Resources/Bullets/simple_enemy_bullet_3D.tres" id="1_itg3a"]
[ext_resource type="Script" uid="uid://b6fmrnipv88bk" path="res://Scripts/Resources/WeaponResource.cs" id="2_6m4qy"]
[resource]
script = ExtResource("2_6m4qy")
Name = &"Enemy weapon with big bullets"
BulletData = ExtResource("1_itg3a")
Priority = 0
AmmoPerShot = 1
RateOfFire = 0.6
BulletCapacity = 4
ReloadTime = 1.0
AutoReload = true
InfiniteAmmo = true
ItemKey = &""
AmmoKey = &""
BulletsPerShot = 1
SpreadAngle = 0.0
RandomSpread = 0.0
_rotationOffset = 0.0

View file

@ -0,0 +1,112 @@
[gd_scene load_steps=18 format=3 uid="uid://bh3vxmqflijgj"]
[ext_resource type="Script" uid="uid://dwregubt4iila" path="res://Scripts/Components/FSM/Enemy/3D/EnemyProxy3D.cs" id="1_a3crc"]
[ext_resource type="Resource" uid="uid://ccym6mcq4fbul" path="res://Resources/Enemies/Fairy_Guard_3D.tres" id="2_jgarc"]
[ext_resource type="Script" uid="uid://c651imhj6rjsh" path="res://Scripts/Components/FSM/Enemy/3D/EnemyStateMachine3D.cs" id="2_xne4s"]
[ext_resource type="Script" uid="uid://cy34e3htvbvnl" path="res://Scripts/Components/FSM/Enemy/3D/Init.cs" id="4_jgarc"]
[ext_resource type="Script" uid="uid://jpdgfn701crh" path="res://Scripts/Components/FSM/Enemy/3D/Idle.cs" id="5_rg1hb"]
[ext_resource type="Script" uid="uid://dvtdw2hcp4rm2" path="res://Scripts/Components/FSM/Enemy/3D/Alert.cs" id="6_jgarc"]
[ext_resource type="Script" uid="uid://crahxykgis2bp" path="res://Scripts/Components/FSM/Enemy/3D/Shooting.cs" id="7_rg1hb"]
[ext_resource type="Script" uid="uid://3irm5sccr2fc" path="res://Scripts/Components/FSM/Enemy/3D/Dead.cs" id="8_5j04l"]
[ext_resource type="Script" uid="uid://mpws3eyrmx0q" path="res://Scripts/Components/FSM/Enemy/3D/Controlled.cs" id="9_dm2sd"]
[ext_resource type="SpriteFrames" uid="uid://ch2ll1on8im2p" path="res://Resources/Sprites/FairyGuard.tres" id="10_hew1j"]
[ext_resource type="Script" uid="uid://de31afbiua8xu" path="res://Scripts/Components/FSM/Enemy/3D/EnemyFSMAnimatedSprite3D.cs" id="11_jgarc"]
[ext_resource type="Script" uid="uid://chq5a73kw0c0m" path="res://Scripts/Components/FSM/Enemy/3D/EnemyStorage3D.cs" id="11_xne4s"]
[ext_resource type="Script" uid="uid://extjdng8nk6r" path="res://Scripts/Components/FSM/Enemy/3D/PlayerDetection3D.cs" id="13_rg1hb"]
[ext_resource type="Script" uid="uid://k5k8wf821ytg" path="res://Scripts/Components/FSM/Enemy/3D/NavigationProvider3D.cs" id="14_dm2sd"]
[ext_resource type="PackedScene" uid="uid://cfgc6ik8vb08c" path="res://Scenes/Weapons/BaseWeapon_3D.tscn" id="15_27vgy"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_jgarc"]
radius = 0.264547
height = 0.935884
[sub_resource type="CylinderShape3D" id="CylinderShape3D_5j04l"]
height = 1.91858
radius = 3.04834
[node name="FairyGuardFsm" type="CharacterBody3D" node_paths=PackedStringArray("EnemyFSM")]
collision_layer = 64
collision_mask = 17
script = ExtResource("1_a3crc")
EnemyFSM = NodePath("StateMachine")
EnemyResource = ExtResource("2_jgarc")
[node name="CollisionShape2D" type="CollisionShape3D" parent="."]
shape = SubResource("CapsuleShape3D_jgarc")
[node name="StateMachine" type="Node" parent="."]
script = ExtResource("2_xne4s")
[node name="Init" type="Node" parent="StateMachine" node_paths=PackedStringArray("Storage", "DetectionProvider")]
script = ExtResource("4_jgarc")
Storage = NodePath("../../Storage")
DetectionProvider = NodePath("../../PlayerDetectionProvider")
[node name="Idle" type="Node" parent="StateMachine" node_paths=PackedStringArray("Storage", "PlayerDetection", "_moduleNodes")]
script = ExtResource("5_rg1hb")
Storage = NodePath("../../Storage")
PlayerDetection = NodePath("../../PlayerDetectionProvider")
DebugEnabled = true
_moduleNodes = [NodePath(""), NodePath(""), NodePath("")]
[node name="Alert" type="Node" parent="StateMachine" node_paths=PackedStringArray("Storage", "PlayerDetection", "NavigationModule", "_moduleNodes")]
script = ExtResource("6_jgarc")
Storage = NodePath("../../Storage")
PlayerDetection = NodePath("../../PlayerDetectionProvider")
NavigationModule = NodePath("../../NavigationProvider")
DebugEnabled = true
_moduleNodes = [NodePath(""), NodePath("")]
[node name="Shooting" type="Node" parent="StateMachine" node_paths=PackedStringArray("Storage", "PlayerDetection", "EquippedWeapon", "NavigationModule", "_moduleNodes")]
script = ExtResource("7_rg1hb")
Storage = NodePath("../../Storage")
PlayerDetection = NodePath("../../PlayerDetectionProvider")
EquippedWeapon = NodePath("../../Weapon")
NavigationModule = NodePath("../../NavigationProvider")
_moduleNodes = [NodePath(""), NodePath("")]
[node name="Dead" type="Node" parent="StateMachine" node_paths=PackedStringArray("Storage")]
script = ExtResource("8_5j04l")
Storage = NodePath("../../Storage")
[node name="Controlled" type="Node" parent="StateMachine" node_paths=PackedStringArray("Storage", "_moduleNodes")]
script = ExtResource("9_dm2sd")
Storage = NodePath("../../Storage")
_moduleNodes = [NodePath("")]
[node name="AnimatedSprite2D" type="AnimatedSprite3D" parent="."]
pixel_size = 0.05
billboard = 1
texture_filter = 0
sprite_frames = ExtResource("10_hew1j")
animation = &"down"
script = ExtResource("11_jgarc")
[node name="Storage" type="Node" parent="." node_paths=PackedStringArray("Root")]
script = ExtResource("11_xne4s")
Root = NodePath("..")
[node name="PlayerDetectionProvider" type="Area3D" parent="."]
collision_layer = 0
collision_mask = 2
script = ExtResource("13_rg1hb")
ObstaclesCollisionMask = 17
[node name="CollisionShape3D" type="CollisionShape3D" parent="PlayerDetectionProvider"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.557434, 0)
shape = SubResource("CylinderShape3D_5j04l")
[node name="NavigationProvider" type="Node" parent="." node_paths=PackedStringArray("NavigationAgent", "StorageModule")]
script = ExtResource("14_dm2sd")
NavigationAgent = NodePath("../NavigationAgent3D")
StorageModule = NodePath("../Storage")
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
path_postprocessing = 1
debug_enabled = true
[node name="Weapon" parent="." instance=ExtResource("15_27vgy")]
[connection signal="body_entered" from="PlayerDetectionProvider" to="PlayerDetectionProvider" method="_on_body_entered"]
[connection signal="body_exited" from="PlayerDetectionProvider" to="PlayerDetectionProvider" method="_on_body_exited"]
[connection signal="velocity_computed" from="NavigationAgent3D" to="NavigationProvider" method="_on_navigation_agent_3d_velocity_computed"]

View file

@ -0,0 +1,96 @@
using Cirno.Scripts.Enums;
using Cirno.Scripts.Utils;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class Alert : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Alert;
[Export] public EnemyStorage3D Storage { get; private set; }
[Export] public PlayerDetection3D PlayerDetection { get; private set; }
[Export] public NavigationProvider3D NavigationModule { get; private set; }
[Export] public bool DebugEnabled { get; set; } = false;
private bool _isPlayerInRange = false;
public override void EnterState()
{
base.EnterState();
// player detection
// damage receiver will be a module
NavigationModule.Init(MainObject);
PlayerDetection.SetRange(Storage.Root.EnemyResource.PlayerDetectionRange);
PlayerDetection.PlayerInRange += PlayerDetectionOnPlayerInRange;
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
GD.Print("Entered alert");
}
private void PlayerDetectionOnPlayerOutOfRange()
{
_isPlayerInRange = false;
}
public override void ExitState()
{
base.ExitState();
PlayerDetection.PlayerInRange -= PlayerDetectionOnPlayerInRange;
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
NavigationModule.SetTarget(null);
// DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
// DamageReceiver.ChangeState(false);
// NavigationModule.SetTarget(null);
}
private void PlayerDetectionOnPlayerInRange()
{
//_isPlayerInRange = true;
}
public override void PhysicsProcessState(double delta)
{
base.PhysicsProcessState(delta);
if (PlayerDetection.IsPlayerInRange(Storage.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight())
{
StateMachine.SetState(EnemyState.Shooting);
return;
}
// if player is outside disengage range, change to idle (later on, search)
if (MainObject.GlobalPosition.DistanceTo(GameController.Instance.PlayerPosition.Value) >=
Storage.Root.EnemyResource.PlayerDisengageRange)
{
StateMachine.SetState(EnemyState.Idle);
return;
}
// Move towards last known position
if (PlayerDetection.LastKnownPlayerPosition.HasValue)
{
MoveTowardsPosition(PlayerDetection.LastKnownPlayerPosition.Value);
}
NavigationModule.Move(Storage.EnemyData.MovementSpeed);
//Storage.FacingDirection = MainObject.Velocity.SnapToCardinal().Normalized();
Storage.FacingDirection = MainObject.Velocity.ToVector2().Normalized();
Storage.AimingDirection = Storage.FacingDirection;
}
private void MoveTowardsPosition(Vector3 position)
{
NavigationModule.SetTarget(position);
}
public override void ProcessState(double delta)
{
base.ProcessState(delta);
}
}

View file

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

View file

@ -0,0 +1,19 @@
using Cirno.Scripts.Enums;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class Controlled : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Controlled;
[Export] public EnemyStorage3D Storage { get; private set; }
public override void EnterState()
{
base.EnterState();
// player detection
// damage receiver will be a module
}
}

View file

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

View file

@ -0,0 +1,19 @@
using Cirno.Scripts.Enums;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class Dead : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Dead;
[Export] public EnemyStorage3D Storage { get; private set; }
public override void EnterState()
{
base.EnterState();
// player detection
// damage receiver will be a module
}
}

View file

@ -0,0 +1 @@
uid://3irm5sccr2fc

View file

@ -0,0 +1,16 @@
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class EnemyFSMAnimatedSprite3D : AnimatedSprite3D
{
public override void _Ready()
{
var enemyFsmProxy = this.GetParentOrNull<EnemyProxy3D>();
if (enemyFsmProxy?.EnemyResource?.AnimationFrames != null)
{
this.SpriteFrames = enemyFsmProxy.EnemyResource.AnimationFrames;
}
}
}

View file

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

View file

@ -0,0 +1,78 @@
using Cirno.Scripts.Enums;
using Cirno.Scripts.Resources;
using Cirno.Scripts.Resources.Loot;
using Godot;
using Godot.Collections;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class EnemyProxy3D : CharacterBody3D, IActivable
{
[Export] public EnemyStateMachine3D EnemyFSM { get; private set; }
[Export] public EnemyResource EnemyResource { get; private set; }
[Export] public Array<LootDrop> ExtraLoot { get; private set; } = [];
[Export] public bool OverrideLoot { get; set; } = false;
[Export]
public AiState StartingAiState { get; private set; }
[ExportCategory("Defeat Script")]
[Export] public Node DefeatScript { get; set; }
[Export] public ActivationType ActivationType { get; private set; } = ActivationType.Toggle;
[Signal] public delegate void DeathEventHandler(EnemyProxy3D enemy);
public void Init(EnemyResource enemyResource)
{
this.EnemyResource = enemyResource;
}
public void TriggerDeath()
{
EmitSignalDeath(this);
}
public void RequestAlert(Vector3 destination)
{
//EnemyFSM.SetState(EnemyState.Alert);
}
public bool Activate(ActivationType activationType = ActivationType.Toggle)
{
switch (activationType)
{
case ActivationType.Toggle:
EnemyFSM.SetState(EnemyState.Controlled);
break;
case ActivationType.Enable:
// Enable or disable AI
EnemyFSM.SetState(EnemyState.Shooting);
break;
case ActivationType.Disable:
EnemyFSM.SetState(EnemyState.Idle);
// Enable or disable AI
break;
case ActivationType.Use:
break;
case ActivationType.Destroy:
EnemyFSM.SetState(EnemyState.Dead);
break;
case ActivationType.Open:
break;
case ActivationType.Close:
break;
}
return true;
}
public void Toggle()
{
this.Activate();
}
}

View file

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

View file

@ -0,0 +1,9 @@
using Cirno.Scripts.Enums;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public abstract partial class EnemyStateBase3D : BaseState<EnemyState, CharacterBody3D>
{
}

View file

@ -0,0 +1 @@
uid://62p771loh356

View file

@ -0,0 +1,10 @@
using Cirno.Scripts.Enums;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class EnemyStateMachine3D : StateMachineBase<EnemyState, CharacterBody3D>
{
[Export] public override EnemyState InitialState { get; protected set; } = EnemyState.Init;
}

View file

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

View file

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using Cirno.Scripts.Resources;
using Cirno.Scripts.Resources.Loot;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class EnemyStorage3D : Node
{
[Export]
public EnemyProxy3D Root { get; private set; }
public Node3D RootAsNode => Root;
public EnemyResource EnemyData => Root.EnemyResource;
public Vector3 HomePosition { get; set; }
public Vector2 MovementDirection { get; set; }
public Vector2 FacingDirection { get; set; }
public Vector2 AimingDirection { get; set; }
public Vector3 KnockbackVelocity { get; set; } = Vector3.Zero;
public float MovementSpeed => Root.EnemyResource.MovementSpeed;
public IEnumerable<LootDrop> LootDrops => Root.OverrideLoot ? Root.ExtraLoot : Root.EnemyResource.LootDrops.Concat(Root.ExtraLoot);
public AiState AiState { get; set; }
}

View file

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

View file

@ -0,0 +1,93 @@
using Cirno.Scripts.Enums;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class Idle : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Idle;
[Export] public EnemyStorage3D Storage { get; private set; }
[Export] public PlayerDetection3D PlayerDetection { get; private set; }
[Export] public bool DebugEnabled { get; set; } = false;
private bool _isPlayerInRange = false;
public override void EnterState()
{
base.EnterState();
PlayerDetection.SetRange(Storage.Root.EnemyResource.PlayerDetectionRange);
_isPlayerInRange = PlayerDetection.IsPlayerInRange(Storage.Root.EnemyResource.ViewRange);
PlayerDetection.PlayerInRange += PlayerDetectionOnPlayerInRange;
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
// player detection
// damage receiver will be a module
GD.Print("Entered Idle");
}
public override void ExitState()
{
base.ExitState();
PlayerDetection.PlayerInRange -= PlayerDetectionOnPlayerInRange;
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
// DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
//
// DamageReceiver.HealthProvider.ResourceDecreased -= HealthProviderOnResourceDecreased;
// DamageReceiver.ChangeState(false);
}
private void HealthProviderOnResourceDepleted()
{
ChangeState(EnemyState.Dead);
}
private void HealthProviderOnResourceDecreased(float oldvalue, float newvalue, float maxvalue)
{
Storage.AiState = AiState.Enabled;
ChangeState(EnemyState.Alert);
}
private void PlayerDetectionOnPlayerInRange()
{
_isPlayerInRange = true;
GD.Print("Player In Range");
}
private void PlayerDetectionOnPlayerOutOfRange()
{
_isPlayerInRange = false;
}
public override void PhysicsProcessState(double delta)
{
base.PhysicsProcessState(delta);
if (Storage.AiState is AiState.Enabled && _isPlayerInRange)
{
if (PlayerDetection.IsPlayerInSight())
{
GD.Print("Moving to alert");
StateMachine.SetState(EnemyState.Alert);
}
}
if (DebugEnabled)
{
DebugDraw3D.DrawText(MainObject.GlobalPosition - new Vector3(0,16,0), "Idle");
}
}
public override void ProcessState(double delta)
{
base.ProcessState(delta);
}
}

View file

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

View file

@ -0,0 +1,25 @@
using Cirno.Scripts.Enums;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class Init : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Init;
[Export] public EnemyStorage3D Storage {get; private set;}
[Export] public PlayerDetection3D DetectionProvider { get; private set; }
public override void EnterState()
{
//DamageReceiver.HealthProvider.MaxResource = StorageModule.Root.EnemyResource.MaxHealth;
DetectionProvider.Initialize(MainObject);
Storage.AiState = Storage.Root.StartingAiState;
Storage.HomePosition = MainObject.GlobalPosition;
// TODO: Hide wings
// TODO: Hide aiming reticule
StateMachine.SetState(EnemyState.Idle);
}
}

View file

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

View file

@ -0,0 +1,84 @@
using Cirno.Scripts.Utils;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class NavigationProvider3D : Node
{
private Vector3? _lastTargetPosition;
private CharacterBody3D _characterBody;
//private NavigationAgent3D _navigationAgent;
[Export] public NavigationAgent3D NavigationAgent { get; private set; }
[Export]
public EnemyStorage3D StorageModule { get; private set; }
public void Init(CharacterBody3D characterBody)
{
_characterBody = characterBody;
if (NavigationAgent is not null) return;
//_navigationAgent = this.GetNode<NavigationAgent3D>("NavigationAgent");
}
public void SetTarget(Vector3? target)
{
_lastTargetPosition = target;
}
public void Move(float movementSpeed)
{
if (!_lastTargetPosition.HasValue)
{
return;
}
NavigationAgent.SetTargetPosition(_lastTargetPosition.Value);
var currentAgentPosition = _characterBody.GlobalPosition;
if (NavigationAgent.IsNavigationFinished())
{
return;
}
var nextPathPosition = NavigationAgent.GetNextPathPosition();
var newVelocity = currentAgentPosition.DirectionTo(nextPathPosition) * movementSpeed;
newVelocity += StorageModule.KnockbackVelocity;
if (NavigationAgent.AvoidanceEnabled)
{
NavigationAgent.SetVelocity(newVelocity);
}
else
{
_on_navigation_agent_3d_velocity_computed(newVelocity);
}
_characterBody.MoveAndSlide();
}
public void _on_navigation_agent_3d_velocity_computed(Vector3 safeVelocity)
{
if (_characterBody is null) return;
_characterBody.Velocity = safeVelocity;
if (_characterBody.Velocity.Length() > 0)
{
StorageModule.FacingDirection = _characterBody.Velocity.Normalized().ToVector2();
}
}
public bool IsNavigable(Vector3 newPos)
{
NavigationAgent.SetTargetPosition(newPos);
return NavigationAgent.IsTargetReachable();
}
public bool IsNavigationFinished()
{
return NavigationAgent.IsNavigationFinished();
}
}

View file

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

View file

@ -0,0 +1,120 @@
using Cirno.Scripts.Components.FSM._3DPlayer;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class PlayerDetection3D : Area3D
{
[Signal]
public delegate void PlayerInRangeEventHandler();
[Signal]
public delegate void PlayerOutOfRangeEventHandler();
[Export(PropertyHint.Layers3DPhysics)]
public uint ObstaclesCollisionMask { get; private set; }
public Vector3? LastKnownPlayerPosition { get; set; }
public Vector3? LastKnowPlayerVelocity { get; set; }
//public bool PlayerInActiveArea { get; private set; }
private CollisionShape3D _collisionShape;
private bool _initialized = false;
// public override void _Ready()
// {
// CallDeferred(MethodName.Initialize);
// }
private CharacterBody3D _mainBody;
public void Initialize(CharacterBody3D mainBody)
{
_mainBody = mainBody;
_initialized = true;
}
public void SetRange(float range)
{
_collisionShape ??= this.GetNode<CollisionShape3D>("CollisionShape3D");
if (_collisionShape.Shape is CylinderShape3D shape3D)
{
shape3D.Radius = range;
}
}
public bool IsPlayerInRange(float range)
{
if (!_initialized) return false;
if (GameController.Instance is null) return false;
if (!GameController.Instance.PlayerPosition.HasValue)
{
return false;
}
return this.GlobalPosition.DistanceTo(GameController.Instance.PlayerPosition.Value) <= range;
}
public bool IsPlayerInSight()
{
if (!_initialized) return false;
if (GameController.Instance is null) return false;
//if (_cachedPlayer == null) return false;
if (!GameController.Instance.PlayerPosition.HasValue) return false;
var found = HasLineOfSight(this.GlobalPosition, GameController.Instance.PlayerPosition.Value);
// var spaceState = GetWorld2D().DirectSpaceState;
//
// // It needs to use its own collision mask because it's detecting obstacles rather than the player
// var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, GameController.Instance.PlayerPosition.Value, ObstaclesCollisionMask,
// [GetRid()]);
// //query.CollideWithBodies = true;
// //query.CollideWithAreas = true;
// // var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, _cachedPlayer.GlobalPosition, CollisionMask, new Array<Rid> { GetRid() });
// var result = spaceState.IntersectRay(query);
//
// // If count is 0 then the player is in sight, otherwise there is level geometry in the way
// var found = result.Count == 0;
if (found)
{
LastKnownPlayerPosition = GameController.Instance.PlayerPosition;
LastKnowPlayerVelocity = GameController.Instance.PlayerVelocity;
}
return found;
}
private void _on_body_entered(Node3D area)
{
if (area is not IsoPlayerFSMProxy player) return;
EmitSignal(SignalName.PlayerInRange);
//PlayerInActiveArea = true;
}
private void _on_body_exited(Node3D area)
{
if (area is not IsoPlayerFSMProxy player) return;
EmitSignal(SignalName.PlayerOutOfRange);
//PlayerInActiveArea = false;
}
public bool HasLineOfSight(Vector3 startPos, Vector3 endPos)
{
var spaceState = GetWorld3D().DirectSpaceState;
// It needs to use its own collision mask because it's detecting obstacles rather than the player
var query = PhysicsRayQueryParameters3D.Create(startPos, endPos, ObstaclesCollisionMask,
[GetRid(), _mainBody.GetRid()]);
query.CollideWithBodies = true;
//query.CollideWithAreas = true;
// var query = PhysicsRayQueryParameters2D.Create(this.GlobalPosition, _cachedPlayer.GlobalPosition, CollisionMask, new Array<Rid> { GetRid() });
var result = spaceState.IntersectRay(query);
// If count is 0 then the player is in sight, otherwise there is level geometry in the way
var found = result.Count == 0;
return found;
}
}

View file

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

View file

@ -0,0 +1,171 @@
using Cirno.Scripts.Enums;
using Cirno.Scripts.Utils;
using Cirno.Scripts.Weapons;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class Shooting : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Shooting;
[Export] public EnemyStorage3D Storage { get; private set; }
[Export] public PlayerDetection3D PlayerDetection { get; private set; }
[Export] public Weapon3D EquippedWeapon;
[Export] public NavigationProvider3D NavigationModule { get; private set; }
private bool _isPlayerInRange = false;
private Vector3? _currentStrafeTarget = null;
private float _strafeSpeed => Storage.EnemyData.StrafeSpeed;
private double _responseTimer = 0;
public override void EnterState()
{
base.EnterState();
GD.Print("Entering Shooting");
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
//DamageReceiver.ChangeState(true);
//DamageReceiver.HealthProvider.ResourceDepleted += HealthProviderOnResourceDepleted;
EquippedWeapon.WeaponData = Storage.Root.EnemyResource.Weapon;
_currentStrafeTarget = null;
_responseTimer = 0;
}
private void PlayerDetectionOnPlayerOutOfRange()
{
GD.Print("Player out of range, returning to alert");
StateMachine.SetState(EnemyState.Alert);
}
public override void ExitState()
{
base.ExitState();
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
//DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
_currentStrafeTarget = null;
//DamageReceiver.ChangeState(false);
}
public override void PhysicsProcessState(double delta)
{
base.PhysicsProcessState(delta);
if (PlayerDetection.IsPlayerInRange(Storage.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight())
{
// SHOOT
Shoot();
if (_strafeSpeed > 0)
{
// Check if a strafe position is needed
if (!_currentStrafeTarget.HasValue || NavigationModule.IsNavigationFinished())
{
if (_responseTimer < Storage.EnemyData.ResponseTime)
{
_responseTimer += delta;
}
else
{
_currentStrafeTarget = CalculateStrafePosition();
_responseTimer = 0;
}
}
}
}
else
{
GD.Print("Back to alert because the player could not be detected anymore");
StateMachine.SetState(EnemyState.Alert);
return;
}
if (_currentStrafeTarget.HasValue)
{
NavigationModule.SetTarget(_currentStrafeTarget.Value);
NavigationModule.Move(_strafeSpeed);
}
}
private Vector3? CalculateStrafePosition()
{
var playerPos = PlayerDetection.LastKnownPlayerPosition.Value;
var enemyPos = MainObject.GlobalPosition;
// Calculate direction to player
var directionToPlayer = (playerPos - enemyPos).Normalized();
// Get perpendicular vectors (left and right strafing directions)
var leftStrafe = directionToPlayer.Rotated(Vector3.Up, -Mathf.Pi / 2);
var rightStrafe = directionToPlayer.Rotated(Vector3.Up,Mathf.Pi / 2);
// Randomly decide left or right first
bool tryLeftFirst = GD.Randf() < 0.5f;
for (float factor = 1f; factor > 0.25f; factor *= 0.75f)
{
float strafeDistance = Mathf.Lerp(Storage.EnemyData.MinStrafeDistance, Storage.EnemyData.MaxStrafeDistance, factor);
var newPos = enemyPos + (tryLeftFirst ? leftStrafe : rightStrafe) * strafeDistance;
if (NavigationModule.IsNavigable(newPos) && PlayerDetection.HasLineOfSight(newPos, playerPos))
{
return newPos;
}
}
for (float factor = 1f; factor > 0.25f; factor *= 0.75f)
{
float strafeDistance = Mathf.Lerp(Storage.EnemyData.MinStrafeDistance, Storage.EnemyData.MaxStrafeDistance, factor);
var newPos = enemyPos + (tryLeftFirst ? rightStrafe : leftStrafe) * strafeDistance;
if (NavigationModule.IsNavigable(newPos) && PlayerDetection.HasLineOfSight(newPos, playerPos))
{
return newPos;
}
}
return null; // No valid strafe position found
}
private Vector2 GetShootDirection()
{
// if (Storage.EnemyData.PredictPlayer && PlayerDetection.LastKnowPlayerVelocity.HasValue)
// {
// var predictedDirection = MathFunctions.PredictInterceptPosition(MainObject.GlobalPosition,
// PlayerDetection.LastKnownPlayerPosition.Value, PlayerDetection.LastKnowPlayerVelocity.Value,
// EquippedWeapon.WeaponData.BulletData.BulletSpeed);
// if (predictedDirection.HasValue) return (predictedDirection.Value - MainObject.GlobalPosition).Normalized();
//
// }
return ( PlayerDetection.LastKnownPlayerPosition.Value - MainObject.GlobalPosition).ToVector2().Normalized();
}
private void Shoot()
{
if (EquippedWeapon == null) return;
if (!PlayerDetection.LastKnownPlayerPosition.HasValue) return;
var direction = GetShootDirection();
// Shoot at the player's last known position
EquippedWeapon.ShootDirection = direction;
//StorageModule.FacingDirection = direction;
Storage.FacingDirection = direction.SnapToCardinal().Normalized();
Storage.AimingDirection = Storage.FacingDirection;
EquippedWeapon.Shoot();
}
}

View file

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

View file

@ -8,39 +8,35 @@ public partial class Alert : EnemyStateBase
{
public override EnemyState StateId => EnemyState.Alert;
[Export]
public EnemyStorageModule StorageModule { get; private set; }
[Export]
public PlayerDetectionModule PlayerDetection { get; private set; }
[Export]
public GenericDamageReceiver DamageReceiver { get; private set; }
[Export]
public NavigationMovementModule NavigationModule { get; private set; }
[Export] public EnemyStorageModule StorageModule { get; private set; }
[Export] public PlayerDetectionModule PlayerDetection { get; private set; }
[Export] public GenericDamageReceiver DamageReceiver { get; private set; }
[Export] public NavigationMovementModule NavigationModule { get; private set; }
private bool _isPlayerInRange = false;
public override void EnterState()
{
base.EnterState();
NavigationModule.Init(MainObject);
PlayerDetection.SetRange(StorageModule.Root.EnemyResource.PlayerDetectionRange);
//_isPlayerInRange = PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange);
//GD.Print($"Player In Range: {_isPlayerInRange}");
PlayerDetection.PlayerInRange += PlayerDetectionOnPlayerInRange;
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
DamageReceiver.ChangeState(true);
DamageReceiver.HealthProvider.ResourceDepleted += HealthProviderOnResourceDepleted;
}
private void HealthProviderOnResourceDepleted()
{
ChangeState(EnemyState.Dead);
@ -55,12 +51,12 @@ public partial class Alert : EnemyStateBase
{
base.ExitState();
PlayerDetection.PlayerInRange -= PlayerDetectionOnPlayerInRange;
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
DamageReceiver.ChangeState(false);
NavigationModule.SetTarget(null);
NavigationModule.SetTarget(null);
}
private void PlayerDetectionOnPlayerInRange()
@ -71,12 +67,13 @@ public partial class Alert : EnemyStateBase
public override void PhysicsProcessState(double delta)
{
base.PhysicsProcessState(delta);
if (PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight())
if (PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange) &&
PlayerDetection.IsPlayerInSight())
{
StateMachine.SetState(EnemyState.Shooting);
return;
}
// if player is outside disengage range, change to idle (later on, search)
if (MainObject.GlobalPosition.DistanceTo(GameManager.Instance.PlayerPosition.Value) >=
StorageModule.Root.EnemyResource.PlayerDisengageRange)
@ -84,13 +81,13 @@ public partial class Alert : EnemyStateBase
StateMachine.SetState(EnemyState.Idle);
return;
}
// Move towards last known position
if (PlayerDetection.LastKnownPlayerPosition.HasValue)
{
MoveTowardsPosition(PlayerDetection.LastKnownPlayerPosition.Value);
}
NavigationModule.Move(StorageModule.EnemyData.MovementSpeed);
StorageModule.FacingDirection = MainObject.Velocity.SnapToCardinal().Normalized();

View file

@ -15,13 +15,13 @@ dest_files=["res://.godot/imported/FairyGuard.png-c13f7bb7cfab625be756e4c8ce7276
[params]
compress/mode=0
compress/mode=3
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/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
@ -31,4 +31,4 @@ 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
detect_3d/compress_to=0

View file

@ -172,6 +172,7 @@ Solid=""
Acid=""
Destroyable=""
navigation_polygon_source_geometry_group=""
navigation_mesh_source_group=""
[importer_defaults]