Add FSM components for player and enemy state management, including initialization and module resolution

This commit is contained in:
MaddoScientisto 2026-02-26 23:13:57 +01:00
commit b6cc5a00e8
57 changed files with 526 additions and 506 deletions

View file

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using Cirno.Scripts.Components.FSM;
using Cirno.Scripts.Components.FSM._3DPlayer;
using Godot;
using Godot.Collections;

View file

@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Cirno.Scripts.Components.FSM;
using Godot;
using GTweens.Builders;
using GTweens.Easings;

View file

@ -1,5 +1,6 @@
using Cirno.Scripts;
using Cirno.Scripts.Components.Actors;
using Cirno.Scripts.Components.FSM;
using Godot;
public partial class EnemyPossessionMovement : ActorFreeMovement

View file

@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Cirno.Scripts.Components.Actors;
using Cirno.Scripts.Components.Actors;
using Cirno.Scripts.Components.Actors._3D;
using Cirno.Scripts.Utils;
using Godot;
@ -14,39 +13,34 @@ public partial class Active : BaseState<PlayerState, CharacterBody3D>
[Export] private InputProvider _inputProvider;
[Export] public PlayerAnimationProvider3D AnimationProvider { get; private set; }
[Export] public IsoPlayerStorageModule Storage { get; private set; }
private IsoPlayerStorageModule Storage { get; set; }
[Export] public PlayerDamageReceiver3D DamageReceiver { get; private set; }
//[Export] public PlayerHitboxSpriteProvider3D HitboxSpriteProvider { get; private set; }
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
base.Init(machine);
//_hud = Hud.Instance;
if (machine is IsoPlayerStateMachine sm)
Storage = sm.Storage;
DamageReceiver.Death += () => { ChangeState(PlayerState.Dead); };
DamageReceiver.HealthDecreased += (value, newValue, maxValue) =>
{
AnimationProvider.Blink();
};
DamageReceiver.HealthDecreased += (_, _, _) =>
{
AnimationProvider.Blink();
};
DamageReceiver.ShieldDecreased += (value, newValue, maxValue) =>
{
AnimationProvider.PlayShieldAnimation();
};
DamageReceiver.ShieldDecreased += (_, _, _) =>
{
AnimationProvider.PlayShieldAnimation();
};
DamageReceiver.Init(StateMachine);
DamageReceiver.RefillHealth();
DamageReceiver.RefillShield();
//_activationProvider.Init(MainObject);
//_weaponProvider.Init(MainObject);
}
public override void EnterState()
@ -78,19 +72,10 @@ public partial class Active : BaseState<PlayerState, CharacterBody3D>
DamageReceiver.Enabled = false;
_canOpenInventory = true;
// _activationProvider.Enabled = false;
// _activation_provider.Enabled = false;
// _interactionController.Enabled = false;
}
public override void PhysicsProcessState(double delta)
{
// Reset at start of frame
//MainObject.Velocity = Vector2.Zero;
// Process modules
base.PhysicsProcessState(delta);
}
public override void ProcessState(double delta)
{
base.ProcessState(delta);
@ -102,14 +87,13 @@ public partial class Active : BaseState<PlayerState, CharacterBody3D>
}
private bool _canOpenInventory = true;
private double _inventoryCooldown = 0;
private double _inventoryCooldown;
private void HandleInputHotkeys(double delta)
{
if (_canOpenInventory && _inputProvider.GetInventoryJustPressed())
{
_canOpenInventory = false;
GameStateManager.SetState(GameState.Inventory);
return;
}
@ -127,7 +111,6 @@ public partial class Active : BaseState<PlayerState, CharacterBody3D>
if (_inputProvider.GetPauseJustPressed())
{
GameStateManager.Instance.Pause();
return;
}
}

View file

@ -1,5 +1,4 @@
using Cirno.Scripts.Components.Actors._3D;
using Cirno.Scripts.Components.FSM.Player;
using Cirno.Scripts.Utils;
using Godot;
@ -11,11 +10,13 @@ public partial class Cutscene : BaseState<PlayerState, CharacterBody3D>
[Export] public PlayerAnimationProvider3D AnimationProvider { get; set; }
[Export] public IsoPlayerStorageModule PlayerStorage { get; private set; }
private IsoPlayerStorageModule PlayerStorage { get; set; }
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
base.Init(machine);
if (machine is IsoPlayerStateMachine sm)
PlayerStorage = sm.Storage;
}
public override void EnterState()
@ -40,15 +41,4 @@ public partial class Cutscene : BaseState<PlayerState, CharacterBody3D>
AnimationProvider.SetAnimation(MainObject.Velocity.ToVector2());
}
public override void PhysicsProcessState(double delta)
{
// Reset at start of frame
//MainObject.Velocity = Vector2.Zero;
// Process modules
base.PhysicsProcessState(delta);
}
}

View file

@ -9,19 +9,34 @@ public partial class Dead : BaseState<PlayerState, CharacterBody3D>
{
public override PlayerState StateId => PlayerState.Dead;
[Export]
private ActorResourceProvider _motivationProvider;
[Export]
private InputProvider _inputProvider;
[Export]
private ActorResourceProvider _healthProvider;
[Export]
private PlayerAnimationProvider3D _animationProvider;
private bool _isGameOver = false;
private bool _isGameOver;
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
base.Init(machine);
// try to obtain common providers via modules/storage
if (machine is IsoPlayerStateMachine sm)
{
var storage = sm.Storage;
// store references if present on storage
_motivationProvider = storage.Root.GetNodeOrNull<ActorResourceProvider>("MotivationProvider");
_healthProvider = storage.Root.GetNodeOrNull<ActorResourceProvider>("HealthProvider");
}
// fallback to searching on the actor
_motivationProvider ??= StateMachine.GetModule<ActorResourceProvider>();
_healthProvider ??= StateMachine.GetModule<ActorResourceProvider>();
_animationProvider ??= StateMachine.GetModule<PlayerAnimationProvider3D>();
}
public override void EnterState()
{
base.EnterState();
@ -50,10 +65,6 @@ public partial class Dead : BaseState<PlayerState, CharacterBody3D>
Hud.Instance.HideTerminated();
}
public override void PhysicsProcessState(double delta)
{
base.PhysicsProcessState(delta);
}
public override void ProcessState(double delta)
{

View file

@ -7,9 +7,14 @@ public partial class Drowning : BaseState<PlayerState, CharacterBody3D>
{
public override PlayerState StateId => PlayerState.Drowning;
[Export]
private PlayerAnimationProvider3D _animationProvider;
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
base.Init(machine);
_animationProvider ??= StateMachine.GetModule<PlayerAnimationProvider3D>();
}
public override void EnterState()
{
_animationProvider.PlayDrowningAnimation();

View file

@ -1,5 +1,4 @@
using System.Threading.Tasks;
using Godot;
using Godot;
namespace Cirno.Scripts.Components.FSM._3DPlayer;
@ -9,9 +8,9 @@ public partial class Init : BaseState<PlayerState, CharacterBody3D>
public override void EnterState()
{
// _storageModule.FacingDirection = ((PlayerStateMachine)StateMachine).StartingDirection;
// _animationProvider.PlayUnteleportAnimation();
_ = AutoSwitchToStart();
// Use Godot timer instead of Task.Delay to stay on main thread
var timer = GetTree().CreateTimer(0.5f);
timer.Timeout += () => StateMachine.SetState(PlayerState.Active);
}
public override void ExitState()
@ -28,10 +27,4 @@ public partial class Init : BaseState<PlayerState, CharacterBody3D>
{
}
private async Task AutoSwitchToStart()
{
await Task.Delay(500);
StateMachine.SetState(PlayerState.Active);
}
}

View file

@ -8,7 +8,7 @@ namespace Cirno.Scripts.Components.FSM._3DPlayer;
public partial class IsoMovementModule : ModuleBase<PlayerState, CharacterBody3D>
{
[Export] public IsoPlayerStorageModule PlayerStorage { get; private set; }
private IsoPlayerStorageModule PlayerStorage { get; set; }
[Export] private InputProvider _inputProvider;
[Export] public PlayerHitboxSpriteProvider3D HitboxSpriteProvider { get; private set; }
@ -29,9 +29,6 @@ public partial class IsoMovementModule : ModuleBase<PlayerState, CharacterBody3D
public int MovementSpeed => _isStrafing ? StrafeSpeed : Speed;
private IStateMachine<PlayerState, CharacterBody3D> _stateMachine;
private CharacterBody3D MainObject => _stateMachine.MainObject;
public override void EnterState(PlayerState state)
{
_accelerationPerSecond = Speed / Acceleration;
@ -44,7 +41,8 @@ public partial class IsoMovementModule : ModuleBase<PlayerState, CharacterBody3D
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
_stateMachine = machine;
base.Init(machine);
PlayerStorage ??= StateMachine.GetModule<IsoPlayerStorageModule>();
}
public override void Process(double delta)

View file

@ -1,9 +1,11 @@
using Cirno.Scripts.Components.Actors;
using Godot;
using Cirno.Scripts.Resources.Loot;
using System.Collections.Generic;
namespace Cirno.Scripts.Components.FSM._3DPlayer;
public partial class IsoPlayerStorageModule : Node
public partial class IsoPlayerStorageModule : Node, IFSMStorage, IActorStorage
{
[Export] public IsoPlayerFSMProxy Root { get; private set; }
@ -14,4 +16,7 @@ public partial class IsoPlayerStorageModule : Node
public Vector2 FacingDirection { get; set; } = Vector2.Down;
public Vector2 AimingDirection { get; set; } = Vector2.Down;
public Vector2 MovementDirection { get; set; } = Vector2.Zero;
// Implement LootDrops for IFSMStorage (players may not drop loot but expose empty list)
public IEnumerable<LootDrop> LootDrops => new LootDrop[0];
}

View file

@ -4,9 +4,6 @@ namespace Cirno.Scripts.Components.FSM._3DPlayer;
public partial class PlayerAcidDeathModule : ModuleBase<PlayerState, CharacterBody3D>
{
private IStateMachine<PlayerState, CharacterBody3D> _stateMachine;
private CharacterBody3D MainObject => _stateMachine.MainObject;
private bool _enabled = false;
public override void EnterState(PlayerState state)
@ -21,7 +18,7 @@ public partial class PlayerAcidDeathModule : ModuleBase<PlayerState, CharacterBo
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
_stateMachine = machine;
base.Init(machine);
}
public override void Process(double delta)
@ -38,6 +35,6 @@ public partial class PlayerAcidDeathModule : ModuleBase<PlayerState, CharacterBo
{
if (!_enabled) return;
GD.Print("Oh no acid");
_stateMachine.SetState(PlayerState.Dead);
StateMachine.SetState(PlayerState.Dead);
}
}

View file

@ -0,0 +1,13 @@
namespace Cirno.Scripts.Components.FSM;
public enum PlayerState
{
Init,
Active,
Cutscene,
Teleporting,
UnTeleporting,
Controlling,
Dead,
Drowning,
}

View file

@ -0,0 +1 @@
uid://7thusbxv3r2n

View file

@ -8,10 +8,7 @@ public partial class PlayerWeaponModule3D : ModuleBase<PlayerState, CharacterBod
[Export] public PlayerWeaponProvider3D WeaponProvider { get; private set; }
[Export] public InputProvider InputProvider { get; private set; }
[Export] public IsoPlayerStorageModule Storage { get; private set; }
private IStateMachine<PlayerState, CharacterBody3D> _stateMachine;
private CharacterBody3D MainObject => _stateMachine.MainObject;
private IsoPlayerStorageModule Storage { get; set; }
public override void EnterState(PlayerState state)
{
@ -25,8 +22,8 @@ public partial class PlayerWeaponModule3D : ModuleBase<PlayerState, CharacterBod
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
_stateMachine = machine;
base.Init(machine);
Storage ??= StateMachine.GetModule<IsoPlayerStorageModule>();
WeaponProvider.Init(MainObject);
}

View file

@ -1,6 +1,4 @@
using Cirno.Scripts.Components.Actors._3D;
using Cirno.Scripts.Components.FSM.Player;
using Cirno.Scripts.Utils;
using Godot;
namespace Cirno.Scripts.Components.FSM._3DPlayer;
@ -9,38 +7,24 @@ public partial class Teleporting : BaseState<PlayerState, CharacterBody3D>
{
public override PlayerState StateId => PlayerState.Teleporting;
[Export] public PlayerAnimationProvider3D AnimationProvider { get; set; }
private PlayerAnimationProvider3D AnimationProvider { get; set; }
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
base.Init(machine);
AnimationProvider ??= StateMachine.GetModule<PlayerAnimationProvider3D>();
}
public override void EnterState()
{
base.EnterState();
AnimationProvider.PlayTeleportAnimation();
AnimationProvider?.PlayTeleportAnimation();
MainObject.Velocity = Vector3.Zero;
}
public override void ExitState()
{
base.ExitState();
}
public override void ProcessState(double delta)
{
base.ProcessState(delta);
}
public override void PhysicsProcessState(double delta)
{
// Process modules
base.PhysicsProcessState(delta);
}
}

View file

@ -1,6 +1,4 @@
using Cirno.Scripts.Components.Actors._3D;
using Cirno.Scripts.Components.FSM.Player;
using Cirno.Scripts.Utils;
using Godot;
namespace Cirno.Scripts.Components.FSM._3DPlayer;
@ -9,37 +7,17 @@ public partial class UnTeleporting : BaseState<PlayerState, CharacterBody3D>
{
public override PlayerState StateId => PlayerState.UnTeleporting;
[Export] public PlayerAnimationProvider3D AnimationProvider { get; set; }
private PlayerAnimationProvider3D AnimationProvider { get; set; }
public override void Init(IStateMachine<PlayerState, CharacterBody3D> machine)
{
base.Init(machine);
AnimationProvider ??= StateMachine.GetModule<PlayerAnimationProvider3D>();
}
public override void EnterState()
{
base.EnterState();
AnimationProvider.PlayUnteleportAnimation();
AnimationProvider?.PlayUnteleportAnimation();
}
public override void ExitState()
{
base.ExitState();
}
public override void ProcessState(double delta)
{
base.ProcessState(delta);
}
public override void PhysicsProcessState(double delta)
{
// Process modules
base.PhysicsProcessState(delta);
}
}

View file

@ -7,15 +7,23 @@ 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; }
private EnemyStorage3D Storage { get; set; }
private PlayerDetection3D PlayerDetection { get; set; }
[Export] public GravityProvider GravityProvider { get; private set; }
private GravityProvider GravityProvider { get; set; }
[Export] public bool DebugEnabled { get; set; } = false;
private bool _isPlayerInRange = false;
public override void Init(IStateMachine<EnemyState, CharacterBody3D> machine)
{
base.Init(machine);
Storage ??= StateMachine.GetModule<EnemyStorage3D>();
PlayerDetection ??= StateMachine.GetModule<PlayerDetection3D>();
GravityProvider ??= StateMachine.GetModule<GravityProvider>();
}
public override void EnterState()
{
base.EnterState();

View file

@ -1,32 +0,0 @@
using Cirno.Scripts.Components.Actors;
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; }
[Export] public ActorResourceProvider HealthProvider { 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
HealthProvider.MaxResource = Storage.Root.EnemyResource.MaxHealth;
HealthProvider.FillResource();
StateMachine.SetState(EnemyState.Idle);
}
}

View file

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

View file

@ -0,0 +1,72 @@
using Cirno.Scripts.Components.Actors;
using Cirno.Scripts.Enums;
using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy._3D;
public partial class InitState : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Init;
private EnemyStorage3D Storage { get; set; }
private PlayerDetection3D DetectionProvider { get; set; }
private ActorResourceProvider HealthProvider { get; set; }
private int _initRetryCount = 0;
private const int MaxInitRetries = 20;
private const float InitRetryDelaySeconds = 0.05f;
public override void Init(IStateMachine<EnemyState, CharacterBody3D> machine)
{
base.Init(machine);
// Try an initial resolve; EnterState will retry if necessary
Storage ??= StateMachine.GetModule<EnemyStorage3D>();
DetectionProvider ??= StateMachine.GetModule<PlayerDetection3D>();
HealthProvider ??= StateMachine.GetModule<ActorResourceProvider>();
}
public override void EnterState()
{
// Attempt to complete initialization; if modules are missing, retry with a timer
TryCompleteInit();
}
private void TryCompleteInit()
{
// Attempt to resolve modules again in case they were attached after Init
Storage ??= StateMachine.GetModule<EnemyStorage3D>();
DetectionProvider ??= StateMachine.GetModule<PlayerDetection3D>();
HealthProvider ??= StateMachine.GetModule<ActorResourceProvider>();
if (Storage is null || DetectionProvider is null || HealthProvider is null)
{
_initRetryCount++;
if (_initRetryCount <= MaxInitRetries)
{
// Schedule a retry shortly to wait for other nodes to finish _Ready
var timer = GetTree().CreateTimer(InitRetryDelaySeconds);
timer.Timeout += TryCompleteInit;
return;
}
// Final failure: log detailed error and bail out to avoid throws
var actorName = MainObject?.Name ?? "<unknown>";
GD.PrintErr($"[InitState] Failed to resolve required modules on actor '{actorName}' after {_initRetryCount} attempts.\n" +
$" Storage: {(Storage is null ? "MISSING" : "OK")},\n" +
$" DetectionProvider: {(DetectionProvider is null ? "MISSING" : "OK")},\n" +
$" HealthProvider: {(HealthProvider is null ? "MISSING" : "OK")}");
return;
}
// All modules present, proceed with initialization
DetectionProvider.Initialize(MainObject);
Storage.AiState = Storage.Root.StartingAiState;
Storage.HomePosition = MainObject.GlobalPosition;
HealthProvider.MaxResource = Storage.Root.EnemyResource.MaxHealth;
HealthProvider.FillResource();
// Transition to the Idle state now that initialization is complete
StateMachine.SetState(EnemyState.Idle);
}
}

View file

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

View file

@ -9,20 +9,30 @@ public partial class Shooting : EnemyStateBase3D
{
public override EnemyState StateId => EnemyState.Shooting;
[Export] public EnemyStorage3D Storage { get; private set; }
private EnemyStorage3D Storage { get; set; }
[Export] public PlayerDetection3D PlayerDetection { get; private set; }
private PlayerDetection3D PlayerDetection { get; set; }
[Export] public Weapon3D EquippedWeapon;
[Export] public NavigationProvider3D NavigationModule { get; private set; }
private NavigationProvider3D NavigationModule { get; set; }
[Export] public GravityProvider GravityProvider { get; private set; }
private GravityProvider GravityProvider { get; set; }
private bool _isPlayerInRange = false;
private Vector3? _currentStrafeTarget = null;
private float _strafeSpeed => Storage.EnemyData.StrafeSpeed;
private bool _isPlayerInRange;
private Vector3? _currentStrafeTarget;
private float StrafeSpeed => Storage.EnemyData.StrafeSpeed;
private double _responseTimer;
public override void Init(IStateMachine<EnemyState, CharacterBody3D> machine)
{
base.Init(machine);
Storage ??= StateMachine.GetModule<EnemyStorage3D>();
PlayerDetection ??= StateMachine.GetModule<PlayerDetection3D>();
NavigationModule ??= StateMachine.GetModule<NavigationProvider3D>();
GravityProvider ??= StateMachine.GetModule<GravityProvider>();
}
private double _responseTimer = 0;
public override void EnterState()
{
base.EnterState();
@ -31,12 +41,13 @@ public partial class Shooting : EnemyStateBase3D
PlayerDetection.PlayerOutOfRange += PlayerDetectionOnPlayerOutOfRange;
//DamageReceiver.ChangeState(true);
//DamageReceiver.HealthProvider.ResourceDepleted += HealthProviderOnResourceDepleted;
if (Storage is null || PlayerDetection is null) return;
EquippedWeapon.WeaponData = Storage.Root.EnemyResource.Weapon;
EquippedWeapon.Init();
if (EquippedWeapon != null)
{
EquippedWeapon.WeaponData = Storage.Root.EnemyResource.Weapon;
EquippedWeapon.Init();
}
_currentStrafeTarget = null;
@ -56,24 +67,22 @@ public partial class Shooting : EnemyStateBase3D
PlayerDetection.PlayerOutOfRange -= PlayerDetectionOnPlayerOutOfRange;
//DamageReceiver.HealthProvider.ResourceDepleted -= HealthProviderOnResourceDepleted;
_currentStrafeTarget = null;
//DamageReceiver.ChangeState(false);
}
public override void PhysicsProcessState(double delta)
{
base.PhysicsProcessState(delta);
if (Storage is null || PlayerDetection is null || NavigationModule is null || GravityProvider is null)
return;
if (PlayerDetection.IsPlayerInRange(Storage.Root.EnemyResource.ViewRange) && PlayerDetection.IsPlayerInSight())
{
// SHOOT
Shoot();
if (_strafeSpeed > 0)
if (StrafeSpeed > 0)
{
// Check if a strafe position is needed
if (!_currentStrafeTarget.HasValue || NavigationModule.IsNavigationFinished())
@ -100,7 +109,7 @@ public partial class Shooting : EnemyStateBase3D
if (_currentStrafeTarget.HasValue)
{
NavigationModule.SetTarget(_currentStrafeTarget.Value);
NavigationModule.Move(_strafeSpeed);
NavigationModule.Move(StrafeSpeed);
}
// Calculate gravity
@ -111,6 +120,8 @@ public partial class Shooting : EnemyStateBase3D
private Vector3? CalculateStrafePosition()
{
if (!PlayerDetection.LastKnownPlayerPosition.HasValue) return null;
var playerPos = PlayerDetection.LastKnownPlayerPosition.Value;
var enemyPos = MainObject.GlobalPosition;
@ -149,16 +160,10 @@ public partial class Shooting : EnemyStateBase3D
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();
if (!PlayerDetection.LastKnownPlayerPosition.HasValue)
return Vector2.Zero;
return (PlayerDetection.LastKnownPlayerPosition.Value - MainObject.GlobalPosition).ToVector2().Normalized();
}
private void Shoot()

View file

@ -8,16 +8,26 @@ public partial class Alert : EnemyStateBase
{
public override EnemyState StateId => EnemyState.Alert;
[Export] public EnemyStorageModule StorageModule { get; private set; }
private EnemyStorageModule StorageModule { get; set; }
[Export] public PlayerDetectionModule PlayerDetection { get; private set; }
private PlayerDetectionModule PlayerDetection { get; set; }
[Export] public GenericDamageReceiver DamageReceiver { get; private set; }
private GenericDamageReceiver DamageReceiver { get; set; }
[Export] public NavigationMovementModule NavigationModule { get; private set; }
private NavigationMovementModule NavigationModule { get; set; }
private bool _isPlayerInRange = false;
public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
{
base.Init(machine);
// Try to resolve modules via the state machine so the same modules can be reused without per-state exports
StorageModule ??= StateMachine.GetModule<EnemyStorageModule>();
PlayerDetection ??= StateMachine.GetModule<PlayerDetectionModule>();
DamageReceiver ??= StateMachine.GetModule<GenericDamageReceiver>();
NavigationModule ??= StateMachine.GetModule<NavigationMovementModule>();
}
public override void EnterState()
{
base.EnterState();
@ -25,8 +35,6 @@ public partial class Alert : EnemyStateBase
PlayerDetection.SetRange(StorageModule.Root.EnemyResource.PlayerDetectionRange);
//_isPlayerInRange = PlayerDetection.IsPlayerInRange(StorageModule.Root.EnemyResource.ViewRange);
//GD.Print($"Player In Range: {_isPlayerInRange}");
PlayerDetection.PlayerInRange += PlayerDetectionOnPlayerInRange;

View file

@ -8,17 +8,14 @@ public partial class Controlled : EnemyStateBase
{
public override EnemyState StateId => EnemyState.Controlled;
[Export]
public EnemyStorageModule StorageModule { get; private set; }
private EnemyStorageModule StorageModule { get; set; }
[Export]
public GenericDamageReceiver DamageReceiver { get; private set; }
private GenericDamageReceiver DamageReceiver { get; set; }
[Export]
private InputProvider _inputProvider;
[Export]
public PlayerCrosshairProvider CrosshairProvider { get; private set; }
private PlayerCrosshairProvider CrosshairProvider { get; set; }
[Export] public Weapon EquippedWeapon { get; private set; }
@ -28,6 +25,14 @@ public partial class Controlled : EnemyStateBase
private bool _isStrafing = false;
public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
{
base.Init(machine);
StorageModule ??= StateMachine.GetModule<EnemyStorageModule>();
DamageReceiver ??= StateMachine.GetModule<GenericDamageReceiver>();
CrosshairProvider ??= StateMachine.GetModule<PlayerCrosshairProvider>();
}
public override void EnterState()
{
base.EnterState();
@ -80,10 +85,6 @@ public partial class Controlled : EnemyStateBase
if (Input.IsActionJustPressed(ControlEndAction))
{
// if (GameManager.Instance.ToggleControlMode() is GameState.Playing)
// {
// ResumeControl();
// }
StateMachine.SetState(EnemyState.Idle);
}
@ -126,7 +127,6 @@ public partial class Controlled : EnemyStateBase
// Shoot at the player's last known position
EquippedWeapon.ShootDirection = direction;
//StorageModule.FacingDirection = direction;
//StorageModule.FacingDirection = direction.SnapToCardinal().Normalized();
EquippedWeapon.Shoot(BulletOwner.Player);

View file

@ -7,18 +7,16 @@ public partial class Dead : EnemyStateBase
{
public override EnemyState StateId => EnemyState.Dead;
[Export]
public EnemyStorageModule StorageModule { get; private set; }
private EnemyStorageModule StorageModule { get; set; }
[Export]
public EnemyDropsProvider DropsProvider { get; private set; }
private EnemyDropsProvider DropsProvider { get; set; }
// public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
// {
// base.Init(machine);
//
//
// }
public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
{
base.Init(machine);
StorageModule ??= StateMachine.GetModule<EnemyStorageModule>();
DropsProvider ??= StateMachine.GetModule<EnemyDropsProvider>();
}
public override void EnterState()
{

View file

@ -3,16 +3,15 @@ using System.Linq;
using Cirno.Scripts.Resources;
using Cirno.Scripts.Resources.Loot;
using Godot;
using Godot.Collections;
namespace Cirno.Scripts.Components.FSM.Enemy;
public partial class EnemyStorageModule : Node2D, IFSMStorage
public partial class EnemyStorageModule : Node2D, IFSMStorage, IActorStorage
{
[Export]
public EnemyFSMProxy Root { get; private set; }
public Node2D RootAsNode => Root;
public Node RootAsNode => Root;
public EnemyResource EnemyData => Root.EnemyResource;

View file

@ -11,21 +11,22 @@ public partial class Idle : EnemyStateBase
// Scan for player, move to alert if found
// Receive damage, move to alert if received
[Export]
public EnemyStorageModule StorageModule { get; private set; }
private EnemyStorageModule StorageModule { get; set; }
[Export]
public PlayerDetectionModule PlayerDetection { get; private set; }
private PlayerDetectionModule PlayerDetection { get; set; }
[Export]
public GenericDamageReceiver DamageReceiver { get; private set; }
private GenericDamageReceiver DamageReceiver { get; set; }
// public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
// {
// base.Init(machine);
// }
private bool _isPlayerInRange = false;
public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
{
base.Init(machine);
StorageModule ??= StateMachine.GetModule<EnemyStorageModule>();
PlayerDetection ??= StateMachine.GetModule<PlayerDetectionModule>();
DamageReceiver ??= StateMachine.GetModule<GenericDamageReceiver>();
}
public override void EnterState()
{
base.EnterState();

View file

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

View file

@ -4,15 +4,20 @@ using Godot;
namespace Cirno.Scripts.Components.FSM.Enemy;
public partial class Init : EnemyStateBase
public partial class InitState : EnemyStateBase
{
public override EnemyState StateId => EnemyState.Init;
[Export]
public GenericDamageReceiver DamageReceiver { get; private set; }
private GenericDamageReceiver DamageReceiver { get; set; }
[Export]
public EnemyStorageModule StorageModule { get; private set; }
private EnemyStorageModule StorageModule { get; set; }
public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
{
base.Init(machine);
DamageReceiver ??= StateMachine.GetModule<GenericDamageReceiver>();
StorageModule ??= StateMachine.GetModule<EnemyStorageModule>();
}
public override void EnterState()
{

View file

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

View file

@ -9,17 +9,13 @@ public partial class Shooting : EnemyStateBase
{
public override EnemyState StateId => EnemyState.Shooting;
[Export]
public EnemyStorageModule StorageModule { get; private set; }
private EnemyStorageModule StorageModule { get; set; }
[Export]
public PlayerDetectionModule PlayerDetection { get; private set; }
private PlayerDetectionModule PlayerDetection { get; set; }
[Export]
public GenericDamageReceiver DamageReceiver { get; private set; }
private GenericDamageReceiver DamageReceiver { get; set; }
[Export]
public NavigationMovementModule NavigationModule { get; private set; }
private NavigationMovementModule NavigationModule { get; set; }
// [Export] public float MaxStrafeDistance { get; private set; } = 64f;
// [Export] public float MinStrafeDistance { get; private set; } = 16f;
@ -34,6 +30,15 @@ public partial class Shooting : EnemyStateBase
private double _responseTimer = 0;
public override void Init(IStateMachine<EnemyState, CharacterBody2D> machine)
{
base.Init(machine);
StorageModule ??= StateMachine.GetModule<EnemyStorageModule>();
PlayerDetection ??= StateMachine.GetModule<PlayerDetectionModule>();
DamageReceiver ??= StateMachine.GetModule<GenericDamageReceiver>();
NavigationModule ??= StateMachine.GetModule<NavigationMovementModule>();
}
public override void EnterState()
{
base.EnterState();

View file

@ -0,0 +1,11 @@
using Godot;
namespace Cirno.Scripts.Components.FSM;
public interface IActorStorage
{
public Vector2 FacingDirection { get; set; }
public Vector2 AimingDirection { get; set; }
public Vector2 MovementDirection { get; set; }
}

View file

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

View file

@ -7,5 +7,5 @@ namespace Cirno.Scripts.Components.FSM;
public interface IFSMStorage
{
public IEnumerable<LootDrop> LootDrops { get; }
public Node2D RootAsNode { get; }
public Node RootAsNode { get; }
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
#nullable enable
using System.Collections.Generic;
using Godot;
namespace Cirno.Scripts.Components.FSM;
@ -15,4 +16,7 @@ public interface IStateMachine<TKey, [MustBeVariant] TType>
public TKey GetState();
public TType MainObject { get; }
// Allow states to query modules by type from the owning actor
public T? GetModule<T>() where T : Node;
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
#nullable enable
using System.Collections.Generic;
using Godot;
namespace Cirno.Scripts.Components.FSM;
@ -8,12 +9,14 @@ public abstract partial class IsoStateMachineBase<TKey, TType> : Node, IStateMac
where TType : Node
{
public Dictionary<TKey, IState<TKey, TType>> States { get; set; } = new();
public TKey CurrentStateIndex { get; set; }
public TKey CurrentStateIndex { get; set; } = default!;
public IState<TKey, TType> CurrentState => States[CurrentStateIndex];
public abstract TKey InitialState { get; protected set; }
private TType _mainObject;
private TType _mainObject = default!;
public TType MainObject => _mainObject;
private bool _hasState;
public override void _Ready()
{
@ -38,23 +41,38 @@ public abstract partial class IsoStateMachineBase<TKey, TType> : Node, IStateMac
public void SetState(TKey stateId)
{
if (CurrentStateIndex is not null)
if (_hasState)
{
// Keep track of previous state and exit it
// PreviousStateIndex is not part of this base; derived classes may add it if needed
CurrentState.ExitState();
}
CurrentStateIndex = stateId;
CurrentState.EnterState();
_hasState = true;
}
// Implement the GetModule<T>() helper so this base class satisfies the IStateMachine interface
public T? GetModule<T>() where T : Node
{
if (MainObject is null) return default;
foreach (var child in MainObject.GetChildren())
{
if (child is T t) return t;
}
return default;
}
public override void _Process(double delta)
{
if (CurrentStateIndex is null) return;
if (!_hasState) return;
CurrentState.ProcessState(delta);
}
public override void _PhysicsProcess(double delta)
{
if (CurrentStateIndex is null) return;
if (!_hasState) return;
CurrentState.PhysicsProcessState(delta);
}
}

View file

@ -6,9 +6,18 @@ public abstract partial class ModuleBase<TKey, TType> : Node, IModule<TKey, TTyp
where TKey : notnull
where TType : Node
{
// Provide the state machine reference for derived modules to use without repeating boilerplate
protected IStateMachine<TKey, TType> StateMachine { get; private set; }
protected TType MainObject => StateMachine.MainObject;
// Default Init behaviour sets the state machine reference. Derived classes can still override
public virtual void Init(IStateMachine<TKey, TType> machine)
{
StateMachine = machine;
}
public abstract void EnterState(TKey state);
public abstract void ExitState(TKey state);
public abstract void Init(IStateMachine<TKey, TType> machine);
public abstract void Process(double delta);
public abstract void PhysicsProcess(double delta);
}

View file

@ -21,19 +21,20 @@ public partial class Active : PlayerStateBase
[Export] public float Acceleration = 8f;
[Export] public float Deceleration = 8f;
[ExportCategory("Providers")] [Export] private PlayerWeaponProvider _weaponProvider;
[Export] private PlayerAnimationProvider _animationProvider;
[Export] private PlayerCrosshairProvider _crosshairProvider;
[Export] private PlayerHitboxSpriteProvider _hitboxSpriteProvider;
// Providers previously exported on the state — resolve them from the actor via the state machine
private PlayerWeaponProvider _weaponProvider;
private PlayerAnimationProvider _animationProvider;
private PlayerCrosshairProvider _crosshairProvider;
private PlayerHitboxSpriteProvider _hitboxSprite_provider;
[Export] private InputProvider _inputProvider;
private InputProvider _inputProvider;
[Export] private PlayerDamageReceiver _damageReceiver;
[Export] private ActivationProvider _activationProvider;
private PlayerDamageReceiver _damageReceiver;
private ActivationProvider _activationProvider;
[Export] private InteractionController _interactionController;
private InteractionController _interaction_controller;
[Export] private PlayerStorageModule _storageModule;
private PlayerStorageModule _storageModule;
private bool _isStrafing;
@ -49,31 +50,46 @@ public partial class Active : PlayerStateBase
_hud = Hud.Instance;
_damageReceiver.Death += () => { ChangeState(PlayerState.Dead); };
// Resolve modules from the actor attached to this state machine
_weaponProvider ??= StateMachine.GetModule<PlayerWeaponProvider>();
_animationProvider ??= StateMachine.GetModule<PlayerAnimationProvider>();
_crosshairProvider ??= StateMachine.GetModule<PlayerCrosshairProvider>();
_hitboxSprite_provider ??= StateMachine.GetModule<PlayerHitboxSpriteProvider>();
_damageReceiver.HealthDecreased += (value, newValue, maxValue) =>
_inputProvider ??= StateMachine.GetModule<InputProvider>();
_damageReceiver ??= StateMachine.GetModule<PlayerDamageReceiver>();
_activationProvider ??= StateMachine.GetModule<ActivationProvider>();
_interaction_controller ??= StateMachine.GetModule<InteractionController>();
_storageModule ??= StateMachine.GetModule<PlayerStorageModule>();
if (_damageReceiver != null)
{
_animationProvider.Blink();
//_hud.UpdateHealth(value, maxValue);
};
_damageReceiver.Death += () => { ChangeState(PlayerState.Dead); };
_damageReceiver.ShieldDecreased += (value, newValue, maxValue) =>
{
_animationProvider.PlayShieldAnimation();
//_hud.UpdateShield(value, maxValue);
};
_damageReceiver.HealthDecreased += (_, _, _) =>
{
_animationProvider?.Blink();
//_hud.UpdateHealth(value, maxValue);
};
_damageReceiver.Init(StateMachine);
_damageReceiver.ShieldDecreased += (_, _, _) =>
{
_animationProvider?.PlayShieldAnimation();
//_hud.UpdateShield(value, maxValue);
};
_damageReceiver.RefillHealth();
_damageReceiver.RefillShield();
_damageReceiver.Init(StateMachine);
_activationProvider.Init(MainObject);
_damageReceiver.RefillHealth();
_damageReceiver.RefillShield();
}
//_weaponProvider = stateMachine.GetNode<PlayerWeaponProvider>("WeaponProvider");
//_animationProvider = stateMachine.GetNode<PlayerAnimationProvider>("AnimationProvider");
_activationProvider?.Init(MainObject);
_weaponProvider.Init(MainObject);
_weaponProvider?.Init(MainObject);
}
public override void EnterState()
@ -81,11 +97,11 @@ public partial class Active : PlayerStateBase
base.EnterState();
// enable sprite
// enable crosshair
_crosshairProvider.Show();
_animationProvider.ShowSprite();
_damageReceiver.Enabled = true;
_activationProvider.Enabled = true;
_interactionController.Enabled = true;
_crosshairProvider?.Show();
_animationProvider?.ShowSprite();
if (_damageReceiver != null) _damageReceiver.Enabled = true;
if (_activationProvider != null) _activationProvider.Enabled = true;
if (_interaction_controller != null) _interaction_controller.Enabled = true;
_accelerationPerSecond = Speed / Acceleration;
_decelerationPerSecond = Speed / Deceleration;
@ -94,14 +110,14 @@ public partial class Active : PlayerStateBase
public override void ExitState()
{
base.ExitState();
_animationProvider.SetAnimationSpeed(Vector2.Zero);
//_animationProvider.SetAnimation(Vector2.Zero);
_crosshairProvider.Hide();
_hitboxSpriteProvider.Hide();
_animationProvider?.SetAnimationSpeed(Vector2.Zero);
//_animation_provider.SetAnimation(Vector2.Zero);
_crosshairProvider?.Hide();
_hitboxSprite_provider?.Hide();
_damageReceiver.Enabled = false;
_activationProvider.Enabled = false;
_interactionController.Enabled = false;
if (_damageReceiver != null) _damageReceiver.Enabled = false;
if (_activationProvider != null) _activationProvider.Enabled = false;
if (_interaction_controller != null) _interaction_controller.Enabled = false;
}
private float _accelerationPerSecond;
@ -147,7 +163,7 @@ public partial class Active : PlayerStateBase
_isStrafing = _inputProvider.GetStrafePressed();
// Toggle visibility of the hitbox sprite based on strafing
_hitboxSpriteProvider.SetVisibility(_isStrafing);
_hitboxSprite_provider?.SetVisibility(_isStrafing);
var rightStickInput = _inputProvider.GetAimInput().Normalized();
@ -164,13 +180,13 @@ public partial class Active : PlayerStateBase
}
// }
_animationProvider.SetAnimationSpeed(MainObject.Velocity);
_animationProvider.SetAnimation(FacingDirection);
_animationProvider?.SetAnimationSpeed(MainObject.Velocity);
_animationProvider?.SetAnimation(FacingDirection);
HandleWeaponSwitch();
_weaponProvider.Update(delta);
_weaponProvider?.Update(delta);
//_crosshairProvider.UpdatePosition(FacingDirection);
//_crosshair_provider.UpdatePosition(FacingDirection);
HandleShoot();
@ -200,28 +216,28 @@ public partial class Active : PlayerStateBase
{
if (_inputProvider.GetReloadJustPressed())
{
_weaponProvider.Reload();
_weaponProvider?.Reload();
return;
}
if (!_inputProvider.GetShootPressed()) return;
_weaponProvider.Shoot(this.FacingDirection);
_weaponProvider?.Shoot(this.FacingDirection);
}
private void HandleInteraction()
{
_activationProvider.HandleInteraction();
_activationProvider?.HandleInteraction();
}
private void HandleWeaponSwitch()
{
if (_inputProvider.GetWeaponNextJustPressed())
{
_weaponProvider.NextWeapon();
_weaponProvider?.NextWeapon();
}
else if (_inputProvider.GetWeaponPreviousJustPressed())
{
_weaponProvider.PreviousWeapon();
_weaponProvider?.PreviousWeapon();
}
}
}

View file

@ -6,11 +6,13 @@ public partial class Drowning : PlayerStateBase
{
public override PlayerState StateId => PlayerState.Drowning;
[Export]
private PlayerAnimationProvider _animationProvider;
public override void EnterState()
{
_animationProvider ??= StateMachine.GetModule<PlayerAnimationProvider>();
if (_animationProvider is null) return;
_animationProvider.PlayDrowningAnimation();
// Wait for animation to be over and switch to death
@ -19,6 +21,7 @@ public partial class Drowning : PlayerStateBase
private void AnimationProviderOnOnAnimationEnded(StringName animationName)
{
if (_animationProvider is null) return;
if (animationName != _animationProvider.DrowningAnimationName) return;
_animationProvider.OnAnimationEnded -= AnimationProviderOnOnAnimationEnded;

View file

@ -1,5 +1,3 @@
using System;
using System.Threading.Tasks;
using Godot;
namespace Cirno.Scripts.Components.FSM.Player;
@ -8,16 +6,27 @@ public partial class Init : PlayerStateBase
{
public override PlayerState StateId => PlayerState.Init;
[Export]
private PlayerAnimationProvider _animationProvider;
[Export] private PlayerStorageModule _storageModule;
private PlayerStorageModule _storageModule;
public override void EnterState()
{
_storageModule.FacingDirection = ((PlayerStateMachine)StateMachine).StartingDirection;
_animationProvider.PlayUnteleportAnimation();
_ = AutoSwitchToStart();
// Resolve modules lazily to avoid method/class name conflicts with Init
_animationProvider ??= StateMachine.GetModule<PlayerAnimationProvider>();
_storageModule ??= StateMachine.GetModule<PlayerStorageModule>();
if (_storageModule != null)
_storageModule.FacingDirection = ((PlayerStateMachine)StateMachine).StartingDirection;
if (_animationProvider != null)
{
_animationProvider.PlayUnteleportAnimation();
// If you need to wait for animation end, subscribe here
}
var timer = GetTree().CreateTimer(0.5f);
timer.Timeout += () => StateMachine.SetState(PlayerState.Active);
}
public override void ExitState()
@ -34,10 +43,4 @@ public partial class Init : PlayerStateBase
{
}
private async Task AutoSwitchToStart()
{
await Task.Delay(500);
StateMachine.SetState(PlayerState.Active);
}
}

View file

@ -1,5 +1,4 @@
using System;
using Cirno.Scripts.Components.Actors;
using Cirno.Scripts.Components.Actors;
using Cirno.Scripts.Resources;
using Godot;
@ -13,7 +12,7 @@ public partial class PlayerFSMItemUseModule : ModuleBase<PlayerState, CharacterB
[Export]
public ActorResourceProvider Shield { get; set; }
[Export] private PlayerStorageModule _storageModule;
private PlayerStorageModule _storageModule;
public Vector2 FacingDirection
{
@ -21,7 +20,7 @@ public partial class PlayerFSMItemUseModule : ModuleBase<PlayerState, CharacterB
private set => _storageModule.FacingDirection = value;
}
public bool Enabled { get; set; } = false;
public bool Enabled { get; set; }
public override void EnterState(PlayerState state)
{
@ -40,6 +39,7 @@ public partial class PlayerFSMItemUseModule : ModuleBase<PlayerState, CharacterB
{
InventoryManager.Instance.ItemUsed += this.UseItem;
_machine = machine;
_storageModule ??= StateMachine.GetModule<PlayerStorageModule>();
}
private void UseItem(LootItem item, int totalcount)
@ -48,94 +48,8 @@ public partial class PlayerFSMItemUseModule : ModuleBase<PlayerState, CharacterB
GD.Print($"Used {item.ItemName} in player");
item.ItemEffect?.Execute(this, item);
return;
// switch (item.Item)
// {
// case ItemTypes.KeycardRed:
// break;
// case ItemTypes.KeycardBlue:
// break;
// case ItemTypes.KeycardGreen:
// break;
// case ItemTypes.Ammo:
// break;
// case ItemTypes.Medkit:
// Heal(item);
// break;
// case ItemTypes.FrogBomb:
// SpawnSpiderBomb(item);
// break;
// case ItemTypes.Bomb:
// SpawnBomb(item);
// break;
// case ItemTypes.Mine:
// break;
// case ItemTypes.Battery:
// RechargeShield(item);
// break;
// case ItemTypes.Weapon:
// break;
// case ItemTypes.Power:
// break;
// case ItemTypes.Points:
// break;
// case ItemTypes.Credits:
// break;
// case ItemTypes.KeyItem:
// break;
// }
}
// private void SpawnBomb(LootItem item)
// {
// GD.Print("Spawned bomb");
// }
//
// private void Heal(LootItem item)
// {
// Health.CurrentResource += Mathf.CeilToInt(Health.MaxResource * 25 / 100);
// }
//
// private void RechargeShield(LootItem item)
// {
// Shield.CurrentResource += Mathf.CeilToInt(Health.MaxResource * 25 / 100);
// }
//
// private void SpawnSpiderBomb(LootItem item)
// {
// //var bullet = item.WeaponData.BulletData.BulletScene.Instantiate<Bullet>();
// //bullet.Initialize(item.WeaponData.BulletData.MakeBullet(this.GlobalPosition, 1,0, 0), GameManager.Instance);
//
//
// //InventoryManager.Instance.RemoveItem(item.ItemKey, 1);
// // emit projectile
// var bullet = this.CreateChildOf<Bullet>(GameManager.Instance.BulletsContainer, item.WeaponData.BulletData.BulletScene, this.GlobalPosition);
//
// var bulletData = item.WeaponData.MakeBullet(this.GlobalPosition);
//
// bullet.Initialize(bulletData, GameManager.Instance);
// bullet.SetDirection(FacingDirection);
// bullet.RotateSpriteDegrees(-90);
// //bullet.SetDirection(_facingDirection);
// bullet.Speed = item.WeaponData.BulletData.BulletSpeed;
//
// _machine.SetState(PlayerState.Controlling);
//
// //RequestMovementDisable(true);
// // set camera
// GameManager.Instance.CameraTargetObject(bullet);
// // set event destroy
// bullet.OnDestroy += () =>
// {
// GameManager.Instance.CameraTargetPlayer();
// _machine.SetState(PlayerState.Active);
// //RequestMovementDisable(false);
// };
// }
public override void Process(double delta)
{

View file

@ -6,12 +6,12 @@ public partial class Teleporting : PlayerStateBase
{
public override PlayerState StateId => PlayerState.Teleporting;
[Export]
private PlayerAnimationProvider _animationProvider;
public override void EnterState()
{
_animationProvider.PlayTeleportAnimation();
_animationProvider ??= StateMachine.GetModule<PlayerAnimationProvider>();
_animationProvider?.PlayTeleportAnimation();
}
public override void ExitState()

View file

@ -4,14 +4,13 @@ namespace Cirno.Scripts.Components.FSM.Player;
public partial class UnTeleporting : PlayerStateBase
{
public override PlayerState StateId => PlayerState.UnTeleporting
;
[Export]
public override PlayerState StateId => PlayerState.UnTeleporting;
private PlayerAnimationProvider _animationProvider;
public override void EnterState()
{
_animationProvider.PlayUnteleportAnimation();
_animationProvider ??= StateMachine.GetModule<PlayerAnimationProvider>();
_animationProvider?.PlayUnteleportAnimation();
}
public override void ExitState()

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
#nullable enable
using System.Collections.Generic;
using Godot;
namespace Cirno.Scripts.Components.FSM;
@ -7,14 +8,21 @@ public abstract partial class StateMachineBase<TKey, TType> : Node, IStateMachin
where TKey : notnull
where TType : Node
{
[Signal]
public delegate void StateChangedEventHandler(Variant from, Variant to);
public Dictionary<TKey, IState<TKey, TType>> States { get; set; } = new();
public TKey CurrentStateIndex { get; set; }
public TKey PreviousStateIndex { get; private set; }
public IState<TKey, TType> CurrentState => States[CurrentStateIndex];
public abstract TKey InitialState { get; protected set; }
private TType _mainObject;
private TType _mainObject = default!;
public TType MainObject => _mainObject;
// Internal flag to indicate if a state has been set previously
private bool _hasState;
public override void _Ready()
{
_mainObject = this.GetParent<TType>();
@ -30,31 +38,62 @@ public abstract partial class StateMachineBase<TKey, TType> : Node, IStateMachin
}
SetState(InitialState);
}
public TKey GetState()
{
return CurrentState.StateId;
}
public void SetState(TKey stateId)
{
if (CurrentStateIndex is not null)
if (_hasState)
{
PreviousStateIndex = CurrentStateIndex;
CurrentState.ExitState();
}
var from = _hasState ? CurrentStateIndex.ToString() : string.Empty;
CurrentStateIndex = stateId;
var to = CurrentStateIndex.ToString();
CurrentState.EnterState();
_hasState = true;
EmitSignal(nameof(StateChanged), from, to);
}
/// <summary>
/// Returns the first descendant under the main object (root node that owns this state machine)
/// that matches the requested type T, or null if none found. This lets states fetch
/// modules attached to the actor without per-state editor wiring.
/// </summary>
public T? GetModule<T>() where T : Node
{
if (MainObject is null) return default;
return FindInChildren<T>(MainObject);
}
private static T? FindInChildren<T>(Node parent) where T : Node
{
foreach (var obj in parent.GetChildren())
{
if (obj is T t) return t;
if (obj is Node node)
{
var found = FindInChildren<T>(node);
if (found is not null) return found;
}
}
return default;
}
public override void _Process(double delta)
{
if (CurrentStateIndex is null) return;
if (!_hasState) return;
CurrentState.ProcessState(delta);
}
public override void _PhysicsProcess(double delta)
{
if (CurrentStateIndex is null) return;
if (!_hasState) return;
CurrentState.PhysicsProcessState(delta);
}
}

View file

@ -8,6 +8,8 @@ using Cirno.Scripts.Components.Actors;
using Cirno.Scripts.Resources;
using Godot.Collections;
using System.Threading.Tasks;
using Cirno.Scripts.Components.FSM;
using Cirno.Scripts.Components.FSM._3DPlayer;
using Cirno.Scripts.Controllers;
using Cirno.Scripts.Utils;
@ -586,14 +588,4 @@ public partial class PlayerMovement : CharacterBody2D, IDestructible
}
public enum PlayerState
{
Init,
Active,
Cutscene,
Teleporting,
UnTeleporting,
Controlling,
Dead,
Drowning,
}

View file

@ -1,4 +1,5 @@
using Godot;
using Cirno.Scripts.Components.FSM;
using Godot;
namespace Cirno.Scripts.Resources.Events;

View file

@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Cirno.Scripts.Components.FSM;
using Godot;
using GTweens.Builders;
using GTweens.Easings;

View file

@ -1,4 +1,5 @@
using Cirno.Scripts.Components.FSM.Player;
using Cirno.Scripts.Components.FSM;
using Cirno.Scripts.Components.FSM.Player;
using Cirno.Scripts.Controllers;
using Godot;