Rotating turret NPC

This commit is contained in:
Marco 2025-03-03 15:01:12 +01:00
commit dc55fa97d3
14 changed files with 401 additions and 27 deletions

View file

@ -14,4 +14,7 @@
<Content Include="Publish.ps1" />
<Content Include="Scripts\Resources\Events\tsconfig.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="Scripts\Components\FSM\Enemy\" />
</ItemGroup>
</Project>

View file

@ -34,7 +34,6 @@ collision_layer = 16
collision_mask = 2
script = ExtResource("3_ax0x5")
SweepSpeed = 10.0
Debug = true
SpritePath = NodePath("../AnimatedSprite2D")
[node name="CollisionShape2D" type="CollisionShape2D" parent="PlayerDetection"]

View file

@ -0,0 +1,169 @@
[gd_scene load_steps=28 format=3 uid="uid://bjskkeb3ppcs8"]
[ext_resource type="Script" uid="uid://c2mo5hc1qb6kf" path="res://Scripts/Components/Actors/Actor.cs" id="1_g7c56"]
[ext_resource type="Texture2D" uid="uid://ke3ialixybfn" path="res://Sprites/Actors/Turret360.png" id="2_g7c56"]
[ext_resource type="Script" uid="uid://tk6ytw246ubg" path="res://Scripts/Components/Actors/EnemyPossessionMovement.cs" id="3_kuwnw"]
[ext_resource type="Script" uid="uid://dq338w2lw5phl" path="res://Scripts/Components/Actors/KeyboardInputProvider.cs" id="4_m663r"]
[ext_resource type="Script" uid="uid://daoxbq4sxy0br" path="res://Scripts/Components/Actors/TurretAnimationModule.cs" id="5_g7c56"]
[ext_resource type="Script" uid="uid://ghq0lmohdvqf" path="res://Scripts/Components/Actors/ActorAi.cs" id="6_5eesc"]
[ext_resource type="Script" uid="uid://c4qmuxjhheahr" path="res://Scripts/Components/ProximityPlayerDetection.cs" id="8_ktwe0"]
[ext_resource type="Script" uid="uid://cqwvssstkrdmw" path="res://Scripts/Components/Actors/ActorResourceProvider.cs" id="9_pyymf"]
[ext_resource type="PackedScene" uid="uid://cj63k0dmk7tl1" path="res://Scenes/Weapons/enemy_weapon_base.tscn" id="10_k6dxy"]
[ext_resource type="Resource" uid="uid://cdfmedtgp2rcn" path="res://Resources/Weapons/EnemyWeapon.tres" id="11_7jc33"]
[ext_resource type="Script" uid="uid://2cijskgyt2xb" path="res://Scripts/Components/Actors/DamageReceiverActorModule.cs" id="12_04r4v"]
[ext_resource type="Script" uid="uid://m0ag88kn0c40" path="res://Scripts/Components/Actors/DeathAnimationHandler.cs" id="13_ufa2c"]
[ext_resource type="Resource" uid="uid://dk2rbf88a5irh" path="res://Resources/Bullets/Explosion_Harmless.tres" id="14_jal4w"]
[ext_resource type="Script" uid="uid://b0qcrs74bdqhf" path="res://Scripts/Components/Actors/EnemyTurretRotationMovement.cs" id="15_g7c56"]
[ext_resource type="Script" uid="uid://7g3luecewcp5" path="res://Scripts/Components/Actors/ActorDefeatScriptHandler.cs" id="15_n6k45"]
[sub_resource type="AtlasTexture" id="AtlasTexture_x4kwn"]
atlas = ExtResource("2_g7c56")
region = Rect2(0, 0, 16, 16)
[sub_resource type="AtlasTexture" id="AtlasTexture_kuwnw"]
atlas = ExtResource("2_g7c56")
region = Rect2(16, 0, 16, 16)
[sub_resource type="AtlasTexture" id="AtlasTexture_m663r"]
atlas = ExtResource("2_g7c56")
region = Rect2(32, 0, 16, 16)
[sub_resource type="AtlasTexture" id="AtlasTexture_84800"]
atlas = ExtResource("2_g7c56")
region = Rect2(48, 0, 16, 16)
[sub_resource type="AtlasTexture" id="AtlasTexture_5eesc"]
atlas = ExtResource("2_g7c56")
region = Rect2(64, 0, 16, 16)
[sub_resource type="AtlasTexture" id="AtlasTexture_hjdpo"]
atlas = ExtResource("2_g7c56")
region = Rect2(80, 0, 16, 16)
[sub_resource type="AtlasTexture" id="AtlasTexture_ktwe0"]
atlas = ExtResource("2_g7c56")
region = Rect2(96, 0, 16, 16)
[sub_resource type="AtlasTexture" id="AtlasTexture_pyymf"]
atlas = ExtResource("2_g7c56")
region = Rect2(112, 0, 16, 16)
[sub_resource type="SpriteFrames" id="SpriteFrames_k6dxy"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_x4kwn")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_kuwnw")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_m663r")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_84800")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_5eesc")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_hjdpo")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ktwe0")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_pyymf")
}],
"loop": true,
"name": &"default",
"speed": 5.0
}]
[sub_resource type="CircleShape2D" id="CircleShape2D_2b36v"]
radius = 5.0
[sub_resource type="CircleShape2D" id="CircleShape2D_sthwe"]
radius = 85.0529
[sub_resource type="CircleShape2D" id="CircleShape2D_0tkae"]
radius = 5.09902
[node name="Turret360" type="CharacterBody2D"]
collision_layer = 16
collision_mask = 113
script = ExtResource("1_g7c56")
MovementSpeed = 30.0
Health = 6.0
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="."]
sprite_frames = SubResource("SpriteFrames_k6dxy")
frame = 2
frame_progress = 0.534265
[node name="MovementProvider" type="Node2D" parent="." node_paths=PackedStringArray("DamageReceiver", "EquippedWeapon")]
script = ExtResource("3_kuwnw")
DamageReceiver = NodePath("../DamageReceiver")
EquippedWeapon = NodePath("../EnemyWeapon")
[node name="InputProvider" type="Node2D" parent="MovementProvider"]
script = ExtResource("4_m663r")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_2b36v")
[node name="AnimationHandler" type="Node2D" parent="." node_paths=PackedStringArray("TurretSprite")]
script = ExtResource("5_g7c56")
TurretSprite = NodePath("../AnimatedSprite2D")
RotationOffset = -90.0
InvertRotation = true
[node name="ActorAi" type="Node2D" parent="."]
script = ExtResource("6_5eesc")
Ai = 0
[node name="PlayerDetection" type="Area2D" parent="."]
visible = false
collision_layer = 0
collision_mask = 2
script = ExtResource("8_ktwe0")
[node name="PlayerDetectionArea" type="CollisionShape2D" parent="PlayerDetection"]
shape = SubResource("CircleShape2D_sthwe")
[node name="HealthProvider" type="Node2D" parent="."]
script = ExtResource("9_pyymf")
ResourceName = "Health"
MaxResource = 6.0
[node name="EnemyWeapon" parent="." instance=ExtResource("10_k6dxy")]
WeaponData = ExtResource("11_7jc33")
[node name="DamageReceiver" type="Node2D" parent="." node_paths=PackedStringArray("HealthProvider")]
script = ExtResource("12_04r4v")
HealthProvider = NodePath("../HealthProvider")
BulletGroup = 2
[node name="Area2D" type="Area2D" parent="DamageReceiver"]
collision_layer = 16
collision_mask = 8
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageReceiver/Area2D"]
shape = SubResource("CircleShape2D_0tkae")
[node name="DeathAnimation" type="Node2D" parent="."]
script = ExtResource("13_ufa2c")
ExplosionData = ExtResource("14_jal4w")
[node name="DefeatScriptHandler" type="Node2D" parent="."]
script = ExtResource("15_n6k45")
[node name="TurretRotationHandler" type="Node2D" parent="." node_paths=PackedStringArray("EquippedWeapon", "_playerDetection")]
script = ExtResource("15_g7c56")
CollisionMask = 81
EquippedWeapon = NodePath("../EnemyWeapon")
_playerDetection = NodePath("../PlayerDetection")
[connection signal="area_entered" from="PlayerDetection" to="PlayerDetection" method="_on_area_entered"]
[connection signal="area_exited" from="PlayerDetection" to="PlayerDetection" method="_on_area_exited"]
[connection signal="area_entered" from="DamageReceiver/Area2D" to="DamageReceiver" method="_on_damage_hitbox_area_entered"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -17,7 +17,7 @@ public partial class ActorFreeMovement : MovementHandler
set => _parent.MovementDirection = value;
}
[Export] public string StrafeAction { get; private set; } = "strafe";
[Export] public StringName StrafeAction { get; private set; } = "strafe";
public bool IsDestroyed => _parent.IsDestroyed;

View file

@ -12,9 +12,9 @@ public partial class EnemyPossessionMovement : ActorFreeMovement
[Export]
public AnimatedSprite2D PossessionSprite { get; private set; }
[Export] public string ControlEndAction { get; private set; } = "pause";
[Export] public StringName ControlEndAction { get; private set; } = "pause";
[Export] public string ShootAction { get; private set; } = "shoot";
[Export] public StringName ShootAction { get; private set; } = "shoot";
[Export] public DamageReceiverActorModule DamageReceiver { get; private set; }

View file

@ -0,0 +1,98 @@
using Godot;
namespace Cirno.Scripts.Components.Actors;
public partial class EnemyTurretRotationMovement : MovementHandler
{
public override Vector2 MovementDirection
{
get => _parent.MovementDirection;
set => _parent.MovementDirection = value;
}
public override Vector2 FacingDirection
{
get => _parent.FacingDirection;
set => _parent.FacingDirection = value;
}
[Export] public float PlayerDisengageRange = 500f;
[Export(PropertyHint.Layers2DPhysics)]
public uint CollisionMask { get; set; }
public bool IsDestroyed => _parent.IsDestroyed;
[Export] public Weapon EquippedWeapon;
[Export] private PlayerDetection _playerDetection;
private Vector2? _lastPlayerPosition = null;
private ActorAi _actorAi;
private bool IsPlayerInRange => _playerDetection is { IsPlayerInRange: true };
private bool IsPlayerInSight =>
_playerDetection is not null && _playerDetection.IsPlayerInSight(CollisionMask);
public override void Init(Actor parent)
{
base.Init(parent);
MovementDirection = Vector2.Zero;
FacingDirection = Vector2.Down;
_actorAi = parent.GetNode<ActorAi>("ActorAi");
}
public override void Update(double delta)
{
}
public override void PhysicsUpdate(double delta)
{
if (IsDestroyed) return;
if (_actorAi.Ai is not AiState.Enabled)
return;
switch (_actorAi.State)
{
case EnemyState.Idle:
if (_playerDetection != null && IsPlayerInSight)
{
_actorAi.State = EnemyState.Alert;
GD.Print("Switching to alert");
}
break;
case EnemyState.Alert:
// Update last known player position if it's in range
if (IsPlayerInRange)
{
_lastPlayerPosition = _playerDetection.CachedPlayer.GlobalPosition;
}
if (IsPlayerInSight)
{
Shoot();
}
break;
}
}
private void Shoot()
{
if (EquippedWeapon == null || !_lastPlayerPosition.HasValue) return;
var direction = (_lastPlayerPosition.Value - _parent.GlobalPosition).Normalized();
// Shoot at the player's last known position
EquippedWeapon.ShootDirection = direction;
FacingDirection = direction;
EquippedWeapon.Shoot();
}
}

View file

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

View file

@ -0,0 +1,59 @@
using Godot;
using System;
namespace Cirno.Scripts.Components.Actors;
public partial class TurretAnimationModule : ActorModule
{
[Export] public AnimatedSprite2D TurretSprite;
[Export(PropertyHint.None, "suffix:°")] public float SweepAngle = 360f;
[Export(PropertyHint.None, "suffix:°")] public float RotationOffset = 0f;
[Export]
public bool InvertRotation = false;
private int _frameCount;
private float _anglePerFrame;
private Actor _actor;
public override void Init(Actor actor)
{
_actor = actor;
if (TurretSprite == null)
{
GD.PushError("TurretAnimationModule requires an AnimatedSprite2D reference.");
return;
}
_frameCount = TurretSprite.SpriteFrames.GetFrameCount(TurretSprite.Animation);
if (_frameCount == 0)
{
GD.PushError("TurretAnimatedSprite2D has no frames in the selected animation.");
return;
}
_anglePerFrame = SweepAngle / _frameCount;
}
public override void Update(double delta) { }
public override void PhysicsUpdate(double delta)
{
if (TurretSprite == null || _frameCount == 0) return;
Vector2 facingDirection = _actor.FacingDirection;
float angle = Mathf.RadToDeg(facingDirection.Angle()) + RotationOffset;
if (InvertRotation)
{
angle = -angle;
}
if (SweepAngle < 360f)
{
angle = Mathf.Clamp(angle, -SweepAngle / 2, SweepAngle / 2);
}
int frame = Mathf.Wrap((int)Mathf.Round(angle / _anglePerFrame), 0, _frameCount);
TurretSprite.Frame = frame;
}
}

View file

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

BIN
Sprites/Actors/Turret360.aseprite (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Sprites/Actors/Turret360.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ke3ialixybfn"
path="res://.godot/imported/Turret360.png-7b7c8fc50f1010b166d70e33da026286.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Sprites/Actors/Turret360.png"
dest_files=["res://.godot/imported/Turret360.png-7b7c8fc50f1010b166d70e33da026286.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