Fixed Shield animation jumps and secondary attack Fixed demon wall stone behavior Made overworld ambient sounds unpausable
224 lines
5.8 KiB
C#
224 lines
5.8 KiB
C#
using Chickensoft.AutoInject;
|
|
using Chickensoft.Introspection;
|
|
using Godot;
|
|
using System;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using Zennysoft.Ma.Adapter;
|
|
using Zennysoft.Ma.Adapter.Entity;
|
|
|
|
namespace Zennysoft.Game.Ma;
|
|
|
|
[Meta(typeof(IAutoNode))]
|
|
public abstract partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLogic>
|
|
{
|
|
#region Registration
|
|
public override void _Notification(int what) => this.Notify(what);
|
|
|
|
protected IEnemyLogic _enemyLogic { get; set; } = default!;
|
|
|
|
IEnemyLogic IProvide<IEnemyLogic>.Value() => _enemyLogic;
|
|
|
|
public EnemyLogic.IBinding EnemyBinding { get; set; } = default!;
|
|
#endregion
|
|
|
|
#region Dependencies
|
|
[Dependency] protected IPlayer _player => this.DependOn(() => GetParent().GetChildren().OfType<IPlayer>().Single());
|
|
#endregion
|
|
|
|
public IHealthComponent HealthComponent { get; private set; }
|
|
|
|
public IAttackComponent AttackComponent { get; private set; }
|
|
|
|
public IDefenseComponent DefenseComponent { get; private set; }
|
|
|
|
public virtual IEnemyModelView EnemyModelView { get; set; } = default!;
|
|
|
|
public Vector3 TargetPosition { get; private set; }
|
|
|
|
[ExportGroup("Enemy Stats")]
|
|
[Export] public int InitialHP { get; set; } = 50;
|
|
|
|
[Export] public int InitialAttack { get; set; } = 8;
|
|
|
|
[Export] public int InitialDefense { get; set; } = 8;
|
|
|
|
[Export] public int ExpGiven { get; set; } = 10;
|
|
|
|
[Node] private AudioStreamPlayer3D _absorbSFX { get; set; } = default!;
|
|
[Node] private AudioStreamPlayer3D _hitSFX { get; set; } = default!;
|
|
[Node] private AudioStreamPlayer3D _morphSFX { get; set; } = default!;
|
|
|
|
protected bool _activated = false;
|
|
private Vector3 _previousPosition = Vector3.Zero;
|
|
|
|
public Enemy()
|
|
{
|
|
HealthComponent = new HealthComponent(InitialHP);
|
|
HealthComponent.HealthReachedZero += Die;
|
|
HealthComponent.DamageTaken += TakeHit;
|
|
|
|
AttackComponent = new AttackComponent(InitialAttack);
|
|
DefenseComponent = new DefenseComponent(InitialDefense);
|
|
}
|
|
|
|
#region Godot methods
|
|
public void Setup()
|
|
{
|
|
_enemyLogic = new EnemyLogic();
|
|
_enemyLogic.Set(this as IEnemy);
|
|
_enemyLogic.Set(_player);
|
|
SetPhysicsProcess(true);
|
|
EnemyModelView.HitPlayer += EnemyModelView_HitPlayer;
|
|
EnemyBinding = _enemyLogic.Bind();
|
|
|
|
EnemyBinding
|
|
.Handle((in EnemyLogic.Output.Activate _) =>
|
|
{
|
|
if (!_activated)
|
|
{
|
|
Activate();
|
|
_activated = true;
|
|
|
|
if (this is IHaveFollowBehavior)
|
|
_enemyLogic.Input(new EnemyLogic.Input.Follow());
|
|
if (this is IHaveFleeBehavior)
|
|
_enemyLogic.Input(new EnemyLogic.Input.Flee());
|
|
}
|
|
})
|
|
.Handle((in EnemyLogic.Output.Idle _) =>
|
|
{
|
|
Idle();
|
|
})
|
|
.Handle((in EnemyLogic.Output.Move _) =>
|
|
{
|
|
Move();
|
|
})
|
|
.Handle((in EnemyLogic.Output.ReturnToDefaultState _) =>
|
|
{
|
|
ReturnToDefaultState();
|
|
});
|
|
|
|
this.Provide();
|
|
|
|
_enemyLogic.Start();
|
|
}
|
|
#endregion
|
|
|
|
public virtual void Activate()
|
|
{
|
|
}
|
|
|
|
public virtual void Idle()
|
|
{
|
|
EnemyModelView.PlayIdleAnimation();
|
|
}
|
|
|
|
public virtual void Move()
|
|
{
|
|
EnemyModelView.PlayWalkAnimation();
|
|
}
|
|
|
|
public virtual void PerformAction()
|
|
{
|
|
EnemyModelView.PlayPrimaryAttackAnimation();
|
|
}
|
|
|
|
public virtual void ReturnToDefaultState()
|
|
{
|
|
|
|
}
|
|
|
|
public virtual void TakeHit()
|
|
{
|
|
_enemyLogic.Input(new EnemyLogic.Input.Alert());
|
|
EnemyModelView.PlayHitAnimation();
|
|
_hitSFX.Play();
|
|
}
|
|
|
|
public virtual void Die()
|
|
{
|
|
SetPhysicsProcess(false);
|
|
_enemyLogic.Input(new EnemyLogic.Input.Defeated());
|
|
_player.ExperiencePointsComponent.Gain(ExpGiven);
|
|
EnemyModelView.PlayDeathAnimation();
|
|
var tweener = CreateTween();
|
|
tweener.TweenInterval(1.0f);
|
|
tweener.TweenCallback(Callable.From(QueueFree));
|
|
}
|
|
|
|
public void OnAbsorb()
|
|
{
|
|
_absorbSFX.Play();
|
|
}
|
|
|
|
public void OnMorph()
|
|
{
|
|
_morphSFX.Play();
|
|
SetPhysicsProcess(false);
|
|
_enemyLogic.Input(new EnemyLogic.Input.Defeated());
|
|
Callable.From(QueueFree);
|
|
}
|
|
|
|
public IDungeonRoom GetCurrentRoom(ImmutableList<IDungeonRoom> roomList)
|
|
{
|
|
foreach (var room in roomList)
|
|
{
|
|
var enemiesInCurrentRoom = room.EnemiesInRoom;
|
|
if (enemiesInCurrentRoom.Contains(this))
|
|
return room;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public void OnExitTree()
|
|
{
|
|
_enemyLogic.Stop();
|
|
EnemyBinding.Dispose();
|
|
HealthComponent.HealthReachedZero -= Die;
|
|
HealthComponent.DamageTaken -= TakeHit;
|
|
EnemyModelView.HitPlayer -= EnemyModelView_HitPlayer;
|
|
}
|
|
|
|
public virtual void MoveEnemyToNewRoom(IDungeonRoom newRoom)
|
|
{
|
|
if (newRoom is MonsterRoom monsterRoom)
|
|
{
|
|
var spawnPoints = monsterRoom.EnemySpawnPoints.GetChildren().OfType<Marker3D>().ToList();
|
|
var spawnPointsGodotCollection = new Godot.Collections.Array<Marker3D>(spawnPoints);
|
|
var randomSpawnPoint = spawnPointsGodotCollection.PickRandom();
|
|
|
|
GlobalPosition = randomSpawnPoint.GlobalPosition;
|
|
|
|
if (this is IHavePatrolBehavior patrolEnemy)
|
|
patrolEnemy.PatrolBehavior.HomePosition = GlobalPosition;
|
|
|
|
_enemyLogic.Input(new EnemyLogic.Input.Reset());
|
|
}
|
|
else
|
|
throw new NotImplementedException($"Only {nameof(MonsterRoom)} types are currently supported.");
|
|
}
|
|
|
|
protected void LookAtTarget(Vector3 targetPosition)
|
|
{
|
|
var velocity = GlobalPosition - _previousPosition;
|
|
if (velocity.IsZeroApprox())
|
|
{
|
|
_previousPosition = GlobalPosition;
|
|
return;
|
|
}
|
|
|
|
var lookDirection = GlobalPosition - targetPosition;
|
|
var look = new Vector3(lookDirection.X, GlobalPosition.Y, lookDirection.Z);
|
|
if (!look.IsEqualApprox(GlobalPosition))
|
|
LookAt(look, Vector3.Up);
|
|
}
|
|
|
|
protected void SetTarget(Vector3 targetPosition) => TargetPosition = targetPosition;
|
|
|
|
private void EnemyModelView_HitPlayer(object sender, EventArgs e)
|
|
{
|
|
_player.TakeDamage(new AttackData(AttackComponent.CurrentAttack.Value, ElementType.None));
|
|
}
|
|
}
|