Files
GameJamDungeon/Zennysoft.Game.Ma/src/player/Player.cs

492 lines
14 KiB
C#

using Chickensoft.AutoInject;
using Chickensoft.Collections;
using Chickensoft.GodotNodeInterfaces;
using Chickensoft.Introspection;
using Chickensoft.SaveFileBuilder;
using Godot;
using SimpleInjector;
using System;
using Zennysoft.Game.Implementation.Components;
using Zennysoft.Ma.Adapter;
using Zennysoft.Ma.Adapter.Entity;
namespace Zennysoft.Game.Ma;
[Meta(typeof(IAutoNode))]
public partial class Player : CharacterBody3D, IPlayer, IProvide<ISaveChunk<PlayerData>>
{
#region Dependency Injection
public override void _Notification(int what) => this.Notify(what);
private PlayerLogic.IBinding PlayerBinding { get; set; } = default!;
[Dependency] private IGameRepo _gameRepo => this.DependOn<IGameRepo>(() => new GameRepo());
#endregion
#region Save
public ISaveChunk<PlayerData> PlayerChunk { get; set; } = default!;
#endregion
public HealthComponent HealthComponent { get; private set; }
public VTComponent VTComponent { get; private set; }
public AttackComponent AttackComponent { get; private set; }
public DefenseComponent DefenseComponent { get; private set; }
public ExperiencePointsComponent ExperiencePointsComponent { get; private set; }
public LuckComponent LuckComponent { get; private set; }
public Vector3 CurrentPosition => GlobalPosition;
public Basis CurrentBasis => Transform.Basis;
public IInventory Inventory { get; private set; } = default!;
public AutoProp<EquipableItem> EquippedWeapon => _equippedWeapon;
private AutoProp<EquipableItem> _equippedWeapon { get; set; } = new AutoProp<EquipableItem>(new Weapon());
public AutoProp<EquipableItem> EquippedArmor => _equippedArmor;
private AutoProp<EquipableItem> _equippedArmor { get; set; } = new AutoProp<EquipableItem>(new Armor());
public AutoProp<EquipableItem> EquippedAccessory => _equippedAccessory;
private AutoProp<EquipableItem> _equippedAccessory { get; set; } = new AutoProp<EquipableItem>(new Accessory());
private PlayerLogic.Settings Settings { get; set; } = default!;
private IPlayerLogic PlayerLogic { get; set; } = default!;
#region Dependencies
[Dependency]
public IGame Game => this.DependOn<IGame>();
[Dependency]
public ISaveChunk<GameData> GameChunk => this.DependOn<ISaveChunk<GameData>>();
ISaveChunk<PlayerData> IProvide<ISaveChunk<PlayerData>>.Value() => PlayerChunk;
#endregion
#region Exports
[ExportGroup("Movement")]
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float RotationSpeed { get; set; } = 1.5f;
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float MoveSpeed { get; set; } = 4f;
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float Acceleration { get; set; } = 2f;
[ExportGroup("Player Stats")]
[Export(PropertyHint.Range, "1, 1000, 1")] public int InitialHP { get; set; } = 212;
[Export(PropertyHint.Range, "0, 1000, 1")] public int InitialVT { get; set; } = 116;
[Export(PropertyHint.Range, "1, 100, 1")] public int InitialAttack { get; set; } = 16;
[Export(PropertyHint.Range, "1, 100, 1")] public int InitialDefense { get; set; } = 12;
[Export(PropertyHint.Range, "1, 100, 1")] public int InitialLuck { get; set; } = 8;
[Export]
private bool HealthTimerIsActive = false;
#endregion
#region Node Dependencies
[Node] private IAnimationPlayer AnimationPlayer { get; set; } = default!;
[Node] private AnimatedSprite2D SwordSlashAnimation { get; set; } = default!;
[Node] private Area3D Hitbox { get; set; } = default!;
[Node] private Area3D CollisionDetector { get; set; } = default!;
[Node] private Timer HealthTimer { get; set; } = default!;
[Node] private RigidBody3D WallCheck { get; set; } = default!;
#endregion
private bool flipAttack = false;
private float _healthTimerWaitTime = 3.0f;
private bool reduceOnTick = true;
private float _knockbackStrength = 0.0f;
private Vector3 _knockbackDirection = Vector3.Zero;
#region Initialization
public void InitializePlayerState()
{
Inventory = new Inventory();
SetProcessInput(false);
SetPhysicsProcess(false);
HealthTimer.WaitTime = _healthTimerWaitTime;
HealthTimer.Timeout += OnHealthTimerTimeout;
}
public void Setup()
{
var container = new SimpleInjector.Container();
container.Register<IPlayerLogic, PlayerLogic>(Lifestyle.Singleton);
PlayerLogic = container.GetInstance<IPlayerLogic>();
PlayerLogic.Set(this as IPlayer);
PlayerLogic.Set(Settings);
PlayerLogic.Set(_gameRepo);
Hitbox.AreaEntered += Hitbox_AreaEntered;
CollisionDetector.AreaEntered += CollisionDetector_AreaEntered;
}
public void OnResolved()
{
Settings = new PlayerLogic.Settings() { RotationSpeed = RotationSpeed, MoveSpeed = MoveSpeed, Acceleration = Acceleration };
PlayerChunk = new SaveChunk<PlayerData>(
onSave: (chunk) => new PlayerData()
{
Inventory = Inventory
},
onLoad: (chunk, data) =>
{
Inventory = data.Inventory;
}
);
PlayerBinding = PlayerLogic.Bind();
PlayerBinding
.Handle((in PlayerLogic.Output.ThrowItem output) =>
{
})
.Handle((in PlayerLogic.Output.Move output) =>
{
Move(output.delta);
});
GameChunk.AddChunk(PlayerChunk);
PlayerLogic.Start();
this.Provide();
SetProcessInput(false);
SetPhysicsProcess(false);
}
public void OnReady()
{
SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2;
HealthComponent = new HealthComponent(InitialHP);
HealthComponent.HealthReachedZero += Die;
VTComponent = new VTComponent(InitialVT);
AttackComponent = new AttackComponent(InitialAttack);
DefenseComponent = new DefenseComponent(InitialDefense);
ExperiencePointsComponent = new ExperiencePointsComponent();
LuckComponent = new LuckComponent(InitialLuck);
}
#endregion
public void Activate()
{
SetProcessInput(true);
SetPhysicsProcess(true);
SetHealthTimerStatus(HealthTimerIsActive);
}
public void Deactivate()
{
SetProcessInput(false);
SetPhysicsProcess(false);
SetHealthTimerStatus(false);
}
public void Attack()
{
if (PlayerIsHittingGeometry())
{
AnimationPlayer.Play("hit_wall");
_gameRepo.OnPlayerAttackedWall();
}
else
{
PlayAttackAnimation();
}
}
public void SetHealthTimerStatus(bool isActive)
{
if (isActive)
HealthTimer.Start();
else
HealthTimer.Stop();
}
public void Move(float delta)
{
var rawInput = GlobalInputVector;
var strafeLeftInput = LeftStrafeInputVector;
var strafeRightInput = RightStrafeInputVector;
var transform = Transform;
transform.Basis = new Basis(Vector3.Up, Settings.RotationSpeed * -rawInput.X * delta) * transform.Basis;
var moveDirection = new Vector3(strafeRightInput - strafeLeftInput, 0, rawInput.Z).Normalized();
var velocity = Basis * moveDirection * Settings.MoveSpeed * Settings.Acceleration;
_knockbackStrength *= 0.9f;
Transform = Transform with { Basis = transform.Basis };
Velocity = velocity + (_knockbackDirection * _knockbackStrength);
MoveAndSlide();
}
public void TeleportPlayer(Transform3D newTransform)
{
Transform = newTransform;
}
public void TakeDamage(Damage damage)
{
var damageReceived = DamageCalculator.CalculateDamage(damage, DefenseComponent.TotalDefense, ((Armor)_equippedArmor.Value).Stats.ElementalResistanceSet);
HealthComponent.Damage(damageReceived);
}
public void Knockback(float impulse)
{
_knockbackStrength = impulse;
_knockbackDirection = GlobalBasis.Z.Normalized();
}
public void LevelUp()
{
var rng = new RandomNumberGenerator();
rng.Randomize();
var hpIncrease = rng.RandiRange(3, 6);
HealthComponent.RaiseMaximumHP(hpIncrease);
ExperiencePointsComponent.LevelUp();
}
public void Die()
{
HealthTimer.WaitTime = _healthTimerWaitTime;
HealthTimer.Timeout -= OnHealthTimerTimeout;
SwordSlashAnimation.Stop();
SetProcessInput(false);
SetPhysicsProcess(false);
Game.GameOver();
}
public override void _Input(InputEvent @event)
{
if (@event.IsActionPressed(GameInputs.Attack))
Attack();
if (@event.IsActionPressed(GameInputs.Sprint))
Settings.MoveSpeed *= 2;
if (@event.IsActionReleased(GameInputs.Sprint))
Settings.MoveSpeed /= 2;
}
public void OnPhysicsProcess(double delta)
{
PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta));
PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition, GlobalTransform));
}
public void Equip(EquipableItem equipable)
{
if (equipable.ItemTag == ItemTag.MysteryItem)
{
var rerolledItem = Game.RerollItem(equipable) as EquipableItem;
Equip(rerolledItem);
return;
}
if (equipable is Weapon weapon)
{
Unequip(_equippedWeapon.Value);
weapon.IsEquipped = true;
_equippedWeapon.OnNext(weapon);
}
else if (equipable is Armor armor)
{
Unequip(_equippedArmor.Value);
armor.IsEquipped = true;
_equippedArmor.OnNext(armor);
}
else if (equipable is Accessory accessory)
{
Unequip(_equippedAccessory.Value);
accessory.IsEquipped = true;
_equippedAccessory.OnNext(accessory);
}
else
throw new NotImplementedException("Item type is not supported.");
}
public void Unequip(EquipableItem equipable)
{
if (equipable is Weapon weapon)
{
weapon.IsEquipped = false;
_equippedWeapon.OnNext(new Weapon());
}
else if (equipable is Armor armor)
{
armor.IsEquipped = false;
_equippedArmor.OnNext(new Armor());
}
else if (equipable is Accessory accessory)
{
accessory.IsEquipped = false;
_equippedAccessory.OnNext(new Accessory());
}
else
throw new NotImplementedException("Item type is not supported.");
}
private static Vector3 GlobalInputVector
{
get
{
var rawInput = Input.GetVector(GameInputs.MoveLeft, GameInputs.MoveRight, GameInputs.MoveUp, GameInputs.MoveDown);
var input = new Vector3
{
X = rawInput.X,
Z = rawInput.Y
};
return input with { Y = 0f };
}
}
private static float LeftStrafeInputVector => Input.GetActionStrength(GameInputs.StrafeLeft);
private static float RightStrafeInputVector => Input.GetActionStrength(GameInputs.StrafeRight);
private void ThrowItem()
{
var itemScene = GD.Load<PackedScene>("res://src/items/throwable/ThrowableItem.tscn");
var throwItem = itemScene.Instantiate<ThrowableItem>();
GetTree().Root.AddChildEx(throwItem);
throwItem.GlobalPosition = CurrentPosition + new Vector3(0, 3.5f, 0);
throwItem.GlobalRotation = GlobalRotation;
}
private void PlayAttackAnimation()
{
var attackSpeed = ((Weapon)EquippedWeapon.Value).AttackSpeed;
AnimationPlayer.SetSpeedScale((float)attackSpeed);
AnimationPlayer.Play("attack");
_gameRepo.OnPlayerAttack();
}
private void OnExitTree()
{
PlayerLogic.Stop();
PlayerBinding.Dispose();
}
private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition;
private void OnHealthTimerTimeout()
{
if (VTComponent.CurrentVT.Value > 0)
{
if (((Accessory)EquippedAccessory.Value).AccessoryTag == AccessoryTag.HalfVTConsumption)
reduceOnTick = !reduceOnTick;
HealthComponent.Heal(1);
if (reduceOnTick)
VTComponent.Reduce(1);
}
else
HealthComponent.Damage(1);
}
private void Hitbox_AreaEntered(Area3D area)
{
var target = area.GetOwner();
if (target is IEnemy enemy)
HitEnemy(enemy);
}
private void HitEnemy(IEnemy enemy)
{
var ignoreElementalResistance = ((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.IgnoreAffinity;
var isCriticalHit = BattleExtensions.IsCriticalHit(LuckComponent.Luck.Value);
var element = ((Weapon)EquippedWeapon.Value).WeaponElement;
var baseAttack = new Damage(
(int)(AttackComponent.TotalAttack * ((Weapon)EquippedWeapon.Value).ElementalDamageBonus),
element,
false,
false,
ignoreElementalResistance);
var damageDealt = DamageCalculator.CalculateDamage(baseAttack, enemy.DefenseComponent.TotalDefense, new ElementalResistanceSet(0, 0, 0, 0, 0));
enemy.TakeDamage(damageDealt);
if (((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.Knockback && enemy is IKnockbackable knockbackable)
knockbackable.Knockback(0.3f, -CurrentBasis.Z.Normalized());
if (((Weapon)EquippedWeapon.Value).WeaponTag == WeaponTag.SelfDamage)
TakeDamage(new Damage(5, ElementType.None, false, true, true));
_gameRepo.OnPlayerAttackedEnemy();
}
private void CollisionDetector_AreaEntered(Area3D area)
{
if (area.GetParent() is InventoryItem inventoryItem)
{
var isAdded = Inventory.TryAdd(inventoryItem);
if (isAdded)
{
_gameRepo.AnnounceMessageOnMainScreen($"{inventoryItem.ItemName} picked up.");
inventoryItem.QueueFree();
}
else
_gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {inventoryItem.ItemName}.");
}
if (area.GetParent() is DroppedItem droppedItem)
{
var isAdded = Inventory.TryAdd(droppedItem.Item);
if (isAdded)
{
_gameRepo.AnnounceMessageOnMainScreen($"{droppedItem.Item.ItemName} picked up.");
droppedItem.QueueFree();
}
else
_gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {droppedItem.Item.ItemName}.");
}
if (area.GetParent() is ThrownItem thrownItem)
{
var isAdded = Inventory.TryAdd(thrownItem.ItemThatIsThrown);
if (isAdded)
{
_gameRepo.AnnounceMessageOnMainScreen($"{thrownItem.ItemThatIsThrown.ItemName} picked up.");
thrownItem.QueueFree();
}
else
_gameRepo.AnnounceMessageOnMainScreen($"Could not pick up {thrownItem.ItemThatIsThrown.ItemName}.");
}
if (area.GetParent() is Restorative restorative)
{
_gameRepo.OnRestorativePickedUp(restorative);
restorative.QueueFree();
}
}
private bool PlayerIsHittingGeometry()
{
var collisions = WallCheck.GetCollidingBodies();
return collisions.Count > 0;
}
private void WallCheck_BodyEntered(Node body)
{
GD.Print("Hit wall");
AnimationPlayer.Stop();
}
}