Major Player refactor

This commit is contained in:
2025-02-07 02:29:50 -08:00
parent 0cdae88952
commit fe1a1e61ef
132 changed files with 2554 additions and 2478 deletions

34
src/player/IPlayer.cs Normal file
View File

@@ -0,0 +1,34 @@
using Godot;
namespace GameJamDungeon;
public interface IPlayer : IKillable
{
public void Attack();
public void ToggleInventory();
public void ToggleMinimap();
public void PlayerPause();
public void TakeDamage(double damage, ElementType elementType = ElementType.None, bool isCriticalHit = false);
public void Knockback(float impulse);
public void GainExp(int expGained);
public void LevelUp();
public void Move(float delta);
public void TeleportPlayer(Vector3 newPosition);
public IInventory Inventory { get; }
public PlayerStats Stats { get; }
public Vector3 CurrentPosition { get; }
public Basis CurrentBasis { get; }
}

View File

@@ -5,248 +5,273 @@ using Chickensoft.SaveFileBuilder;
using Godot;
using Godot.Collections;
namespace GameJamDungeon
namespace GameJamDungeon;
[Meta(typeof(IAutoNode))]
public partial class Player : CharacterBody3D, IPlayer
{
public interface IPlayer : ICharacterBody3D, IKillable
#region Dependency Injection
public override void _Notification(int what) => this.Notify(what);
private PlayerLogic.IBinding PlayerBinding { get; set; } = default!;
#endregion
public double CurrentHP => Stats.CurrentHP.Value;
public Vector3 CurrentPosition => GlobalPosition;
public Basis CurrentBasis => Transform.Basis;
public PlayerStats Stats { get; set; } = default!;
public IInventory Inventory { get; private set; } = default!;
private PlayerLogic.Settings Settings { get; set; } = default!;
private PlayerLogic PlayerLogic { get; set; } = default!;
#region Dependencies
[Dependency]
public IAppRepo AppRepo => this.DependOn<IAppRepo>();
[Dependency]
public IGame Game => this.DependOn<IGame>();
[Dependency]
public ISaveChunk<GameData> GameChunk => this.DependOn<ISaveChunk<GameData>>();
#endregion
#region Event Signals
[Signal]
public delegate void InventoryButtonPressedEventHandler();
[Signal]
public delegate void MinimapButtonHeldEventHandler();
[Signal]
public delegate void PauseButtonPressedEventHandler();
#endregion
#region Exports
[Export]
public PlayerStatResource PlayerStatResource { get; set; } = default!;
[Export]
private WeaponStats _defaultWeapon { get; set; } = default!;
[Export]
private ArmorStats _defaultArmor { get; set; } = default!;
#endregion
#region Node Dependencies
[Node] private IAnimationPlayer AnimationPlayer { get; set; } = default!;
[Node] private AnimatedSprite2D SwordSlashAnimation { get; set; } = default!;
[Node] private IHitbox Hitbox { get; set; } = default!;
[Node] private Timer HealthTimer { 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;
private Dictionary<int, int> _expToNextLevel;
#region Initialization
public void Initialize()
{
public Vector3 GetGlobalInputVector();
public float GetLeftStrafeInputVector();
public float GetRightStrafeInputVector();
public void ApplyCentralImpulseToPlayer(Vector3 velocity);
public void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false);
public void LevelUp();
event Player.InventoryButtonPressedEventHandler InventoryButtonPressed;
event Player.MinimapButtonHeldEventHandler MinimapButtonHeld;
event Player.PauseButtonPressedEventHandler PauseButtonPressed;
AnimationPlayer.AnimationFinished += OnAnimationFinished;
_expToNextLevel = new Dictionary<int, int>
{
{ 2, 12 },
{ 3, 39 },
{ 4, 87 },
{ 5, 162 },
{ 6, 270 },
{ 7, 417 },
{ 8, 609 }
};
}
[Meta(typeof(IAutoNode))]
public partial class Player : CharacterBody3D, IPlayer, IProvide<PlayerLogic>
public void Setup()
{
public override void _Notification(int what) => this.Notify(what);
Settings = new PlayerLogic.Settings() { RotationSpeed = PlayerStatResource.RotationSpeed, MoveSpeed = PlayerStatResource.MoveSpeed, Acceleration = PlayerStatResource.Acceleration };
PlayerLogic IProvide<PlayerLogic>.Value() => PlayerLogic;
Stats = new PlayerStats();
Inventory = new Inventory();
public PlayerLogic.Settings Settings { get; set; } = default!;
Stats.SetCurrentHP(PlayerStatResource.CurrentHP);
Stats.SetMaximumHP(PlayerStatResource.MaximumHP);
Stats.SetCurrentVT(PlayerStatResource.CurrentVT);
Stats.SetMaximumVT(PlayerStatResource.MaximumVT);
Stats.SetCurrentAttack(PlayerStatResource.CurrentAttack);
Stats.SetBonusAttack(PlayerStatResource.BonusAttack);
Stats.SetMaxAttack(PlayerStatResource.MaxAttack);
Stats.SetCurrentDefense(PlayerStatResource.CurrentDefense);
Stats.SetBonusDefense(PlayerStatResource.BonusDefense);
Stats.SetMaxDefense(PlayerStatResource.MaxDefense);
Stats.SetCurrentExp(PlayerStatResource.CurrentExp);
Stats.SetCurrentLevel(PlayerStatResource.CurrentLevel);
Stats.SetExpToNextLevel(PlayerStatResource.ExpToNextLevel);
Stats.SetLuck(PlayerStatResource.Luck);
private PlayerLogic PlayerLogic { get; set; } = default!;
PlayerLogic = new PlayerLogic();
PlayerLogic.Set(this as IPlayer);
PlayerLogic.Set(Settings);
PlayerLogic.Set(AppRepo);
PlayerLogic.Set(Stats);
private PlayerLogic.IBinding PlayerBinding { get; set; } = default!;
var defaultWeapon = new Weapon();
defaultWeapon.SetItemStats(_defaultWeapon);
var defaultArmor = new Armor();
defaultArmor.SetItemStats(_defaultArmor);
Inventory.TryAdd(defaultWeapon);
Inventory.TryAdd(defaultArmor);
#region Dependencies
[Dependency]
public IAppRepo AppRepo => this.DependOn<IAppRepo>();
Inventory.Equip(defaultWeapon);
Inventory.Equip(defaultArmor);
[Dependency]
public IGameRepo GameRepo => this.DependOn<IGameRepo>();
Inventory.EquippedAccessory.Sync += EquippedAccessory_Sync;
Stats.CurrentHP.Sync += CurrentHP_Sync;
Stats.CurrentExp.Sync += CurrentEXP_Sync;
[Dependency]
public ISaveChunk<GameData> GameChunk => this.DependOn<ISaveChunk<GameData>>();
#endregion
HealthTimer.WaitTime = _healthTimerWaitTime;
}
#region Event Signals
[Signal]
public delegate void InventoryButtonPressedEventHandler();
[Signal]
public delegate void MinimapButtonHeldEventHandler();
[Signal]
public delegate void PauseButtonPressedEventHandler();
#endregion
public void OnResolved()
{
PlayerBinding = PlayerLogic.Bind();
#region Exports
[Export]
public PlayerStatResource PlayerStatResource { get; set; } = default!;
[Export]
private WeaponStats _defaultWeapon { get; set; } = default!;
[Export]
private ArmorStats _defaultArmor { get; set; } = default!;
#endregion
#region Node Dependencies
[Node] private IAnimationPlayer AnimationPlayer { get; set; } = default!;
[Node] private AnimatedSprite2D SwordSlashAnimation { get; set; } = default!;
[Node] private IHitbox Hitbox { get; set; } = default!;
[Node] private Timer HealthTimer { get; set; } = default!;
#endregion
private PlayerData PlayerData { get; set; } = default!;
private bool flipAttack = false;
private float _healthTimerWaitTime = 3.0f;
private bool reduceOnTick = true;
private float _knockbackStrength = 0.0f;
private Vector3 _knockbackDirection = Vector3.Zero;
private Dictionary<int, int> _expToNextLevel;
public void Initialize()
{
AnimationPlayer.AnimationFinished += OnAnimationFinished;
_expToNextLevel = new Dictionary<int, int>
PlayerBinding
.Handle((in PlayerLogic.Output.Animations.Attack output) =>
{
{ 2, 12 },
{ 3, 39 },
{ 4, 87 },
{ 5, 162 },
{ 6, 270 },
{ 7, 417 },
{ 8, 609 }
};
}
var attackSpeed = Inventory.EquippedWeapon.Value.AttackSpeed;
AnimationPlayer.SetSpeedScale((float)attackSpeed);
public void Setup()
{
Settings = new PlayerLogic.Settings() { RotationSpeed = PlayerStatResource.RotationSpeed, MoveSpeed = PlayerStatResource.MoveSpeed, Acceleration = PlayerStatResource.Acceleration };
PlayerData = new PlayerData()
AnimationPlayer.Play("attack");
})
.Handle((in PlayerLogic.Output.ThrowItem output) =>
{
GlobalTransform = GlobalTransform,
StateMachine = PlayerLogic,
Velocity = Velocity,
Inventory = new Inventory(),
};
});
PlayerData.SetCurrentHP(PlayerStatResource.CurrentHP);
PlayerData.SetMaximumHP(PlayerStatResource.MaximumHP);
PlayerData.SetCurrentVT(PlayerStatResource.CurrentVT);
PlayerData.SetMaximumVT(PlayerStatResource.MaximumVT);
PlayerData.SetCurrentAttack(PlayerStatResource.CurrentAttack);
PlayerData.SetMaxAttack(PlayerStatResource.MaxAttack);
PlayerData.SetCurrentDefense(PlayerStatResource.CurrentDefense);
PlayerData.SetMaxDefense(PlayerStatResource.MaxDefense);
PlayerData.SetCurrentExp(PlayerStatResource.CurrentExp);
PlayerData.SetCurrentLevel(PlayerStatResource.CurrentLevel);
PlayerData.SetExpToNextLevel(PlayerStatResource.ExpToNextLevel);
PlayerData.SetLuck(PlayerStatResource.Luck);
this.Provide();
PlayerLogic.Start();
HealthTimer.Timeout += OnHealthTimerTimeout;
Inventory.AccessoryUnequipped += Inventory_AccessoryUnequipped;
Hitbox.AreaEntered += Hitbox_AreaEntered;
}
PlayerLogic = new PlayerLogic();
PlayerLogic.Set(this as IPlayer);
PlayerLogic.Set(Settings);
PlayerLogic.Set(AppRepo);
PlayerLogic.Set(GameRepo);
PlayerLogic.Set(PlayerData);
public void OnReady()
{
SetPhysicsProcess(true);
SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2;
}
#endregion
var defaultWeapon = new Weapon() { WeaponStats = _defaultWeapon };
var defaultArmor = new Armor() { ArmorStats = _defaultArmor };
PlayerData.Inventory.TryAdd(defaultWeapon);
PlayerData.Inventory.TryAdd(defaultArmor);
public void Attack()
{
PlayerLogic.Input(new PlayerLogic.Input.Attack());
}
PlayerData.Inventory.Equip(defaultWeapon);
PlayerData.Inventory.Equip(defaultArmor);
public void ToggleInventory()
{
Game.ToggleInventory();
}
PlayerData.Inventory.EquippedAccessory.Sync += EquippedAccessory_Sync;
PlayerData.CurrentHP.Sync += CurrentHP_Sync;
PlayerData.CurrentExp.Sync += CurrentEXP_Sync;
public void ToggleMinimap()
{
Game.ToggleMinimap();
}
HealthTimer.WaitTime = _healthTimerWaitTime;
}
public void PlayerPause()
{
EmitSignal(SignalName.PauseButtonPressed);
}
public void OnResolved()
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);
var velocity = Basis * moveDirection * Settings.MoveSpeed * Settings.Acceleration;
_knockbackStrength = _knockbackStrength * 0.9f;
Transform = Transform with { Basis = transform.Basis };
Velocity = velocity + (_knockbackDirection * _knockbackStrength);
MoveAndSlide();
}
public void TeleportPlayer(Vector3 newPosition)
{
GlobalPosition = newPosition;
}
public void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false)
{
if (Stats.CurrentHP.Value > 0)
{
PlayerBinding = PlayerLogic.Bind();
PlayerBinding
.Handle((in PlayerLogic.Output.MovementComputed output) =>
{
_knockbackStrength = _knockbackStrength * 0.9f;
Transform = Transform with { Basis = output.Rotation };
Velocity = output.Velocity + (_knockbackDirection * _knockbackStrength);
MoveAndSlide();
})
.Handle((in PlayerLogic.Output.Animations.Attack output) =>
{
var attackSpeed = PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.AttackSpeed;
AnimationPlayer.SetSpeedScale((float)attackSpeed);
AnimationPlayer.Play("attack");
})
.Handle((in PlayerLogic.Output.ThrowItem output) =>
{
});
this.Provide();
PlayerLogic.Start();
GameRepo.SetPlayerData(PlayerData);
GlobalPosition = GameRepo.PlayerGlobalPosition.Value;
GameRepo.PlayerGlobalPosition.Sync += PlayerGlobalPosition_Sync;
HealthTimer.Timeout += OnHealthTimerTimeout;
PlayerData.Inventory.AccessoryUnequipped += Inventory_AccessoryUnequipped;
Hitbox.AreaEntered += Hitbox_AreaEntered;
damage = CalculateDefenseResistance(damage);
if (isCriticalHit)
damage *= 2;
Stats.SetCurrentHP(Stats.CurrentHP.Value - (int)damage);
}
}
public void OnReady()
{
SetPhysicsProcess(true);
}
public void Knockback(float impulse)
{
_knockbackStrength = impulse;
_knockbackDirection = GlobalBasis.Z.Normalized();
}
public void TakeDamage(double damage, ElementType elementType, bool isCriticalHit = false)
{
if (PlayerData.CurrentHP.Value > 0)
{
damage = CalculateDefenseResistance(damage);
if (isCriticalHit)
damage *= 2;
PlayerData.SetCurrentHP(PlayerData.CurrentHP.Value - (int)damage);
}
}
public void GainExp(int expGained)
{
Stats.SetCurrentExp(Stats.CurrentExp.Value + expGained);
}
public void LevelUp()
{
var nextLevel = PlayerData.CurrentLevel.Value + 1;
var expToNextLevel = _expToNextLevel[nextLevel];
var newCurrentExp = Mathf.Max(PlayerData.CurrentExp.Value - PlayerData.ExpToNextLevel.Value, 0);
PlayerData.SetCurrentLevel(nextLevel);
PlayerData.SetExpToNextLevel(expToNextLevel);
PlayerData.SetCurrentExp(newCurrentExp);
}
public void LevelUp()
{
var nextLevel = Stats.CurrentLevel.Value + 1;
var expToNextLevel = _expToNextLevel[nextLevel];
var newCurrentExp = Mathf.Max(Stats.CurrentExp.Value - Stats.ExpToNextLevel.Value, 0);
Stats.SetCurrentLevel(nextLevel);
Stats.SetExpToNextLevel(expToNextLevel);
Stats.SetCurrentExp(newCurrentExp);
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed(GameInputs.Inventory))
{
GD.Print("Inventory button pressed");
EmitSignal(SignalName.InventoryButtonPressed);
}
public void Die() => PlayerLogic.Input(new PlayerLogic.Input.Killed());
if (@event.IsActionPressed(GameInputs.MiniMap))
{
GD.Print("MiniMap button pressed");
EmitSignal(SignalName.MinimapButtonHeld);
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed(GameInputs.Inventory))
ToggleInventory();
if (@event.IsActionPressed(GameInputs.Pause))
{
GD.Print("Pause button pressed");
EmitSignal(SignalName.PauseButtonPressed);
}
if (@event.IsActionPressed(GameInputs.MiniMap))
ToggleMinimap();
if (@event.IsActionPressed(GameInputs.Attack))
PlayerLogic.Input(new PlayerLogic.Input.Attack());
}
if (@event.IsActionPressed(GameInputs.Pause))
PlayerPause();
public void OnPhysicsProcess(double delta)
{
PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta));
if (@event.IsActionPressed(GameInputs.Attack))
Attack();
}
SwordSlashAnimation.Position = GetViewport().GetVisibleRect().Size / 2;
public void OnPhysicsProcess(double delta)
{
PlayerLogic.Input(new PlayerLogic.Input.PhysicsTick(delta));
PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition, GlobalTransform));
}
PlayerLogic.Input(new PlayerLogic.Input.Moved(GlobalPosition, GlobalTransform));
}
public Vector3 GetGlobalInputVector()
private static Vector3 GlobalInputVector
{
get
{
var rawInput = Input.GetVector(GameInputs.MoveLeft, GameInputs.MoveRight, GameInputs.MoveUp, GameInputs.MoveDown);
var input = new Vector3
@@ -256,113 +281,117 @@ namespace GameJamDungeon
};
return input with { Y = 0f };
}
}
public float GetLeftStrafeInputVector()
private static float LeftStrafeInputVector
{
get
{
var leftStrafe = Input.GetActionStrength(GameInputs.StrafeLeft);
return leftStrafe;
}
public float GetRightStrafeInputVector()
{
var rightStrafe = Input.GetActionStrength(GameInputs.StrafeRight);
return rightStrafe;
}
public void ThrowItem()
{
var itemScene = GD.Load<PackedScene>("res://src/items/throwable/ThrowableItem.tscn");
var throwItem = itemScene.Instantiate<ThrowableItem>();
GetTree().Root.AddChildEx(throwItem);
throwItem.GlobalPosition = GameRepo.PlayerGlobalPosition.Value;
throwItem.GlobalRotation = GlobalRotation;
}
public void ApplyCentralImpulseToPlayer(Vector3 velocity)
{
_knockbackStrength = 115.0f;
_knockbackDirection = velocity;
}
public void OnAnimationFinished(StringName animation)
{
GD.Print("Attack finished");
PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished());
}
public void OnExitTree()
{
PlayerLogic.Stop();
PlayerBinding.Dispose();
AnimationPlayer.AnimationFinished -= OnAnimationFinished;
}
public void Kill() => PlayerLogic.Input(new PlayerLogic.Input.Killed());
private void PlayerGlobalPosition_Sync(Vector3 newPlayerPosition)
{
GlobalPosition = newPlayerPosition;
}
private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition;
private void OnHealthTimerTimeout()
{
if (PlayerData.CurrentVT.Value > 0)
{
if (PlayerData.Inventory.EquippedAccessory.Value.AccessoryStats.AccessoryTags.Contains(AccessoryTag.HalfVTConsumption))
{
reduceOnTick = !reduceOnTick;
}
PlayerData.SetCurrentHP(PlayerData.CurrentHP.Value + 1);
if (reduceOnTick)
PlayerData.SetCurrentVT(PlayerData.CurrentVT.Value - 1);
}
else
PlayerData.SetCurrentHP(PlayerData.CurrentHP.Value - 1);
}
private void EquippedAccessory_Sync(Accessory equippedItem)
{
PlayerData.SetMaximumHP(PlayerData.MaximumHP.Value + equippedItem.AccessoryStats.MaxHPUp);
PlayerData.SetMaximumVT(PlayerData.MaximumVT.Value + equippedItem.AccessoryStats.MaxVTUp);
PlayerData.SetLuck(PlayerData.Luck.Value + equippedItem.AccessoryStats.LUCKUp);
}
private void Inventory_AccessoryUnequipped(AccessoryStats unequippedAccessory)
{
PlayerData.SetMaximumHP(PlayerData.MaximumHP.Value - unequippedAccessory.MaxHPUp);
PlayerData.SetMaximumVT(PlayerData.MaximumVT.Value - unequippedAccessory.MaxVTUp);
PlayerData.SetLuck(PlayerData.Luck.Value - unequippedAccessory.LUCKUp);
}
private void CurrentHP_Sync(int newHealth)
{
if (newHealth <= 0)
Kill();
}
private void CurrentEXP_Sync(int newExp)
{
if (PlayerData.CurrentExp.Value >= PlayerData.ExpToNextLevel.Value)
LevelUp();
}
private double CalculateDefenseResistance(double incomingDamage)
{
return Mathf.Max(incomingDamage - (PlayerData.CurrentDefense.Value + PlayerData.BonusDefense), 0.0);
}
private void Hitbox_AreaEntered(Area3D area)
{
var target = area.GetOwner();
if (target is IEnemy enemy)
{
enemy.TakeDamage(
(PlayerStatResource.CurrentAttack + PlayerStatResource.BonusAttack + PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.Damage) * PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.ElementalDamageBonus,
PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.WeaponElement,
ignoreElementalResistance: PlayerData.Inventory.EquippedWeapon.Value.WeaponStats.WeaponTags.Contains(WeaponTag.IgnoreAffinity));
}
return Input.GetActionStrength(GameInputs.StrafeLeft);
}
}
private static float RightStrafeInputVector
{
get
{
return 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;
throwItem.GlobalRotation = GlobalRotation;
}
private void OnAnimationFinished(StringName animation)
{
GD.Print("Attack finished");
PlayerLogic.Input(new PlayerLogic.Input.AttackAnimationFinished());
}
private void OnExitTree()
{
PlayerLogic.Stop();
PlayerBinding.Dispose();
AnimationPlayer.AnimationFinished -= OnAnimationFinished;
}
private void OnPlayerPositionUpdated(Vector3 globalPosition) => GlobalPosition = globalPosition;
private void OnHealthTimerTimeout()
{
if (Stats.CurrentVT.Value > 0)
{
if (Inventory.EquippedAccessory.Value.AccessoryTags.Contains(AccessoryTag.HalfVTConsumption))
{
reduceOnTick = !reduceOnTick;
}
Stats.SetCurrentHP(Stats.CurrentHP.Value + 1);
if (reduceOnTick)
Stats.SetCurrentVT(Stats.CurrentVT.Value - 1);
}
else
Stats.SetCurrentHP(Stats.CurrentHP.Value - 1);
}
private void EquippedAccessory_Sync(Accessory accessory)
{
Stats.SetMaximumHP(Stats.MaximumHP.Value + accessory.MaxHPUp);
Stats.SetMaximumVT(Stats.MaximumVT.Value + accessory.MaxVTUp);
Stats.SetLuck(Stats.Luck.Value + accessory.LuckUp);
}
private void Inventory_AccessoryUnequipped(Accessory accessory)
{
Stats.SetMaximumHP(Stats.MaximumHP.Value - accessory.MaxHPUp);
Stats.SetMaximumVT(Stats.MaximumVT.Value - accessory.MaxVTUp);
Stats.SetLuck(Stats.Luck.Value - accessory.LuckUp);
}
private void CurrentHP_Sync(int newHealth)
{
if (newHealth <= 0)
Die();
}
private void CurrentEXP_Sync(int newExp)
{
if (Stats.CurrentExp.Value >= Stats.ExpToNextLevel.Value)
LevelUp();
}
private double CalculateDefenseResistance(double incomingDamage)
{
return Mathf.Max(incomingDamage - Stats.CurrentDefense.Value, 0.0);
}
private void Hitbox_AreaEntered(Area3D area)
{
var target = area.GetOwner();
if (target is IEnemy enemy)
HitEnemy(enemy);
}
private void HitEnemy(IEnemy enemy)
{
var attackValue = PlayerStatResource.CurrentAttack + Inventory.EquippedWeapon.Value.Damage;
var ignoreElementalResistance = Inventory.EquippedWeapon.Value.WeaponTags.Contains(WeaponTag.IgnoreAffinity);
var isCriticalHit = BattleExtensions.IsCriticalHit(Stats.Luck.Value);
var element = Inventory.EquippedWeapon.Value.WeaponElement;
enemy.TakeDamage(
attackValue * Inventory.EquippedWeapon.Value.ElementalDamageBonus,
element,
isCriticalHit,
false,
ignoreElementalResistance);
if (Inventory.EquippedWeapon.Value.WeaponTags.Contains(WeaponTag.Knockback))
enemy.Knockback(0.3f, -CurrentBasis.Z.Normalized());
}
}

View File

@@ -26,8 +26,6 @@ CurrentAttack = 8
CurrentDefense = 12
MaxAttack = 8
MaxDefense = 12
BonusAttack = 0
BonusDefense = 0
Luck = 0.05
[sub_resource type="BoxShape3D" id="BoxShape3D_wedu3"]
@@ -344,7 +342,6 @@ animations = [{
}]
[node name="Player" type="CharacterBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2, 0)
collision_layer = 806
collision_mask = 775
script = ExtResource("1_xcol5")

View File

@@ -1,115 +0,0 @@
using Chickensoft.Collections;
using Chickensoft.Serialization;
using Godot;
namespace GameJamDungeon
{
public partial record PlayerData
{
// TODO: Implement save system
[Save("global_transform")]
public required Transform3D GlobalTransform { get; init; }
[Save("state_machine")]
public required PlayerLogic StateMachine { get; init; }
[Save("velocity")]
public required Vector3 Velocity { get; init; }
[Save("inventory")]
public required Inventory Inventory { get; init; }
[Save("currentHP")]
public IAutoProp<int> CurrentHP => _currentHP;
[Save("maximumHP")]
public IAutoProp<int> MaximumHP => _maximumHP;
[Save("currentVT")]
public IAutoProp<int> CurrentVT => _currentVT;
[Save("maximumVT")]
public IAutoProp<int> MaximumVT => _maximumVT;
[Save("currentExp")]
public IAutoProp<int> CurrentExp => _currentExp;
[Save("currentLevel")]
public IAutoProp<int> CurrentLevel => _currentLevel;
[Save("currentAttack")]
public IAutoProp<int> CurrentAttack => _currentAttack;
[Save("currentDefense")]
public IAutoProp<int> CurrentDefense => _currentDefense;
[Save("maxAttack")]
public IAutoProp<int> MaxAttack => _maxAttack;
[Save("maxDefense")]
public IAutoProp<int> MaxDefense => _maxDefense;
public int BonusAttack => Inventory.EquippedWeapon.Value.WeaponStats.Damage + Inventory.EquippedAccessory.Value.AccessoryStats.ATKUp;
public int BonusDefense => Inventory.EquippedArmor.Value.ArmorStats.Defense + Inventory.EquippedAccessory.Value.AccessoryStats.DEFUp;
[Save("expToNextLevel")]
public IAutoProp<int> ExpToNextLevel => _expToNextLevel;
[Save("luck")]
public IAutoProp<double> Luck => _luck;
public void SetCurrentHP(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaximumHP.Value);
_currentHP.OnNext(clampedValue);
}
public void SetMaximumHP(int newValue)
{
_maximumHP.OnNext(newValue);
}
public void SetCurrentVT(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaximumVT.Value);
_currentVT.OnNext(clampedValue);
}
public void SetMaximumVT(int newValue)
{
_maximumVT.OnNext(newValue);
}
public void SetCurrentExp(int newValue)
{
_currentExp.OnNext(newValue);
}
public void SetCurrentLevel(int newValue)
{
_currentLevel.OnNext(newValue);
}
public void SetCurrentAttack(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaxAttack.Value);
_currentAttack.OnNext(clampedValue);
}
public void SetCurrentDefense(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaxDefense.Value);
_currentDefense.OnNext(clampedValue);
}
public void SetMaxAttack(int newValue)
{
_maxAttack.OnNext(newValue);
}
public void SetMaxDefense(int newValue)
{
_maxDefense.OnNext(newValue);
}
public void SetExpToNextLevel(int newValue)
{
_expToNextLevel.OnNext(newValue);
}
public void SetLuck(double newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, 1.0);
_luck.OnNext(clampedValue);
}
private readonly AutoProp<int> _currentHP = new(int.MaxValue);
private readonly AutoProp<int> _maximumHP = new(int.MaxValue);
private readonly AutoProp<int> _currentVT = new(int.MaxValue);
private readonly AutoProp<int> _maximumVT = new(int.MaxValue);
private readonly AutoProp<int> _currentExp = new(int.MaxValue);
private readonly AutoProp<int> _currentLevel = new(int.MaxValue);
private readonly AutoProp<int> _currentAttack = new(int.MaxValue);
private readonly AutoProp<int> _currentDefense = new(int.MaxValue);
private readonly AutoProp<int> _maxAttack = new(int.MaxValue);
private readonly AutoProp<int> _maxDefense = new(int.MaxValue);
private readonly AutoProp<int> _bonusAttack = new(int.MaxValue);
private readonly AutoProp<int> _bonusDefense = new(int.MaxValue);
private readonly AutoProp<int> _expToNextLevel = new(int.MaxValue);
private readonly AutoProp<double> _luck = new(double.MaxValue);
}
}

View File

@@ -1,42 +1,43 @@
using Godot;
namespace GameJamDungeon
namespace GameJamDungeon;
[GlobalClass]
public partial class PlayerStatResource : Resource
{
[GlobalClass]
public partial class PlayerStatResource : Resource
{
/// <summary>Rotation speed (quaternions?/sec).</summary>
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float RotationSpeed { get; set; } = 12.0f;
/// <summary>Rotation speed (quaternions?/sec).</summary>
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float RotationSpeed { get; set; } = 12.0f;
/// <summary>Player speed (meters/sec).</summary>
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float MoveSpeed { get; set; } = 8f;
/// <summary>Player speed (meters/sec).</summary>
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float MoveSpeed { get; set; } = 8f;
/// <summary>Player speed (meters^2/sec).</summary>
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float Acceleration { get; set; } = 4f;
/// <summary>Player speed (meters^2/sec).</summary>
[Export(PropertyHint.Range, "0, 100, 0.1")]
public float Acceleration { get; set; } = 4f;
[Export] public int CurrentHP { get; set; }
[Export] public int MaximumHP { get; set; }
[Export] public int CurrentHP { get; set; }
[Export] public int MaximumHP { get; set; }
[Export] public int CurrentVT { get; set; }
[Export] public int MaximumVT { get; set; }
[Export] public int CurrentVT { get; set; }
[Export] public int MaximumVT { get; set; }
[Export] public int CurrentExp { get; set; }
[Export] public int ExpToNextLevel { get; set; }
[Export] public int CurrentLevel { get; set; }
[Export] public int CurrentExp { get; set; }
[Export] public int ExpToNextLevel { get; set; }
[Export] public int CurrentLevel { get; set; }
[Export] public int CurrentAttack { get; set; }
[Export] public int CurrentDefense { get; set; }
[Export] public int CurrentAttack { get; set; }
[Export] public int BonusAttack { get; set; }
[Export] public int MaxAttack { get; set; }
[Export] public int MaxDefense { get; set; }
[Export] public int MaxAttack { get; set; }
[Export] public int BonusAttack { get; set; }
[Export] public int BonusDefense { get; set; }
[Export] public int CurrentDefense { get; set; }
[Export(PropertyHint.Range, "0, 1, 0.01")]
public double Luck { get; set; }
}
[Export] public int BonusDefense { get; set; }
[Export] public int MaxDefense { get; set; }
[Export(PropertyHint.Range, "0, 1, 0.01")]
public double Luck { get; set; }
}

114
src/player/PlayerStats.cs Normal file
View File

@@ -0,0 +1,114 @@
using Chickensoft.Collections;
using Chickensoft.Serialization;
using Godot;
namespace GameJamDungeon;
public partial record PlayerStats
{
[Save("currentHP")]
public IAutoProp<int> CurrentHP => _currentHP;
[Save("maximumHP")]
public IAutoProp<int> MaximumHP => _maximumHP;
[Save("currentVT")]
public IAutoProp<int> CurrentVT => _currentVT;
[Save("maximumVT")]
public IAutoProp<int> MaximumVT => _maximumVT;
[Save("currentExp")]
public IAutoProp<int> CurrentExp => _currentExp;
[Save("currentLevel")]
public IAutoProp<int> CurrentLevel => _currentLevel;
[Save("currentAttack")]
public IAutoProp<int> CurrentAttack => _currentAttack;
[Save("bonusAttack")]
public IAutoProp<int> BonusAttack => _bonusAttack;
[Save("maxAttack")]
public IAutoProp<int> MaxAttack => _maxAttack;
[Save("currentDefense")]
public IAutoProp<int> CurrentDefense => _currentDefense;
[Save("bonusDefense")]
public IAutoProp<int> BonusDefense => _bonusDefense;
[Save("maxDefense")]
public IAutoProp<int> MaxDefense => _maxDefense;
[Save("expToNextLevel")]
public IAutoProp<int> ExpToNextLevel => _expToNextLevel;
[Save("luck")]
public IAutoProp<double> Luck => _luck;
public void SetCurrentHP(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaximumHP.Value);
_currentHP.OnNext(clampedValue);
}
public void SetMaximumHP(int newValue)
{
_maximumHP.OnNext(newValue);
}
public void SetCurrentVT(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaximumVT.Value);
_currentVT.OnNext(clampedValue);
}
public void SetMaximumVT(int newValue)
{
_maximumVT.OnNext(newValue);
}
public void SetCurrentExp(int newValue)
{
_currentExp.OnNext(newValue);
}
public void SetCurrentLevel(int newValue)
{
_currentLevel.OnNext(newValue);
}
public void SetCurrentAttack(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaxAttack.Value);
_currentAttack.OnNext(clampedValue);
}
public void SetBonusAttack(int newValue)
{
_bonusAttack.OnNext(newValue);
}
public void SetMaxAttack(int newValue)
{
_maxAttack.OnNext(newValue);
}
public void SetCurrentDefense(int newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, MaxDefense.Value);
_currentDefense.OnNext(clampedValue);
}
public void SetBonusDefense(int newValue)
{
_bonusDefense.OnNext(newValue);
}
public void SetMaxDefense(int newValue)
{
_maxDefense.OnNext(newValue);
}
public void SetExpToNextLevel(int newValue)
{
_expToNextLevel.OnNext(newValue);
}
public void SetLuck(double newValue)
{
var clampedValue = Mathf.Clamp(newValue, 0, 1.0);
_luck.OnNext(clampedValue);
}
private readonly AutoProp<int> _currentHP = new(int.MaxValue);
private readonly AutoProp<int> _maximumHP = new(int.MaxValue);
private readonly AutoProp<int> _currentVT = new(int.MaxValue);
private readonly AutoProp<int> _maximumVT = new(int.MaxValue);
private readonly AutoProp<int> _currentExp = new(int.MaxValue);
private readonly AutoProp<int> _currentLevel = new(int.MaxValue);
private readonly AutoProp<int> _currentAttack = new(int.MaxValue);
private readonly AutoProp<int> _bonusAttack = new(int.MaxValue);
private readonly AutoProp<int> _maxAttack = new(int.MaxValue);
private readonly AutoProp<int> _currentDefense = new(int.MaxValue);
private readonly AutoProp<int> _bonusDefense = new(int.MaxValue);
private readonly AutoProp<int> _maxDefense = new(int.MaxValue);
private readonly AutoProp<int> _expToNextLevel = new(int.MaxValue);
private readonly AutoProp<double> _luck = new(double.MaxValue);
}

View File

@@ -1,22 +1,21 @@
using Godot;
namespace GameJamDungeon
namespace GameJamDungeon;
public partial class PlayerLogic
{
public partial class PlayerLogic
public static class Input
{
public static class Input
{
public readonly record struct PhysicsTick(double Delta);
public readonly record struct PhysicsTick(double Delta);
public readonly record struct Moved(Vector3 GlobalPosition, Transform3D GlobalTransform);
public readonly record struct Moved(Vector3 GlobalPosition, Transform3D GlobalTransform);
public readonly record struct Enable;
public readonly record struct Enable;
public readonly record struct Attack;
public readonly record struct Attack;
public readonly record struct AttackAnimationFinished;
public readonly record struct AttackAnimationFinished;
public readonly record struct Killed;
}
public readonly record struct Killed;
}
}

View File

@@ -1,19 +1,14 @@
using Godot;
namespace GameJamDungeon;
namespace GameJamDungeon
public partial class PlayerLogic
{
public partial class PlayerLogic
public static class Output
{
public static class Output
public static class Animations
{
public static class Animations
{
public readonly record struct Attack;
}
public readonly record struct MovementComputed(Basis Rotation, Vector3 Velocity);
public readonly record struct ThrowItem;
public readonly record struct Attack;
}
public readonly record struct ThrowItem;
}
}

View File

@@ -1,14 +1,13 @@
namespace GameJamDungeon
namespace GameJamDungeon;
public partial class PlayerLogic
{
public partial class PlayerLogic
public record Settings
{
public record Settings
{
public float MoveSpeed { get; set; }
public float MoveSpeed { get; set; }
public float RotationSpeed { get; set; }
public float RotationSpeed { get; set; }
public float Acceleration { get; set; }
}
public float Acceleration { get; set; }
}
}

View File

@@ -1,11 +1,10 @@
using Chickensoft.Introspection;
using Chickensoft.LogicBlocks;
namespace GameJamDungeon
namespace GameJamDungeon;
public partial class PlayerLogic
{
public partial class PlayerLogic
{
[Meta]
public abstract partial record State : StateLogic<State>;
}
[Meta]
public abstract partial record State : StateLogic<State>;
}

View File

@@ -1,14 +1,13 @@
using Chickensoft.Introspection;
using Chickensoft.LogicBlocks;
namespace GameJamDungeon
{
public interface IPlayerLogic : ILogicBlock<PlayerLogic.State>;
namespace GameJamDungeon;
[Meta, Id("player_logic")]
[LogicBlock(typeof(State), Diagram = true)]
public partial class PlayerLogic : LogicBlock<PlayerLogic.State>, IPlayerLogic
{
public override Transition GetInitialState() => To<State.Idle>();
}
public interface IPlayerLogic : ILogicBlock<PlayerLogic.State>;
[Meta, Id("player_logic")]
[LogicBlock(typeof(State), Diagram = true)]
public partial class PlayerLogic : LogicBlock<PlayerLogic.State>, IPlayerLogic
{
public override Transition GetInitialState() => To<State.Idle>();
}

View File

@@ -8,14 +8,12 @@ state "PlayerLogic State" as GameJamDungeon_PlayerLogic_State {
state "Disabled" as GameJamDungeon_PlayerLogic_State_Disabled
}
GameJamDungeon_PlayerLogic_State_Alive --> GameJamDungeon_PlayerLogic_State_Alive : Moved
GameJamDungeon_PlayerLogic_State_Alive --> GameJamDungeon_PlayerLogic_State_Alive : PhysicsTick
GameJamDungeon_PlayerLogic_State_Alive --> GameJamDungeon_PlayerLogic_State_Dead : Killed
GameJamDungeon_PlayerLogic_State_Attacking --> GameJamDungeon_PlayerLogic_State_Idle : AttackAnimationFinished
GameJamDungeon_PlayerLogic_State_Disabled --> GameJamDungeon_PlayerLogic_State_Idle : Enable
GameJamDungeon_PlayerLogic_State_Idle --> GameJamDungeon_PlayerLogic_State_Attacking : Attack
GameJamDungeon_PlayerLogic_State_Alive : OnPhysicsTick → MovementComputed
GameJamDungeon_PlayerLogic_State_Idle : OnAttack → Attack
[*] --> GameJamDungeon_PlayerLogic_State_Idle

View File

@@ -1,18 +1,17 @@
using Chickensoft.Introspection;
namespace GameJamDungeon
namespace GameJamDungeon;
public partial class PlayerLogic
{
public partial class PlayerLogic
public partial record State
{
public partial record State
[Meta]
public partial record Attacking : Alive, IGet<Input.AttackAnimationFinished>
{
[Meta]
public partial record Attacking : Alive, IGet<Input.AttackAnimationFinished>
public Transition On(in Input.AttackAnimationFinished input)
{
public Transition On(in Input.AttackAnimationFinished input)
{
return To<Idle>();
}
return To<Idle>();
}
}
}

View File

@@ -1,22 +1,21 @@
using Chickensoft.Introspection;
using Godot;
namespace GameJamDungeon
{
public partial class PlayerLogic
{
public abstract partial record State
{
[Meta, Id("player_logic_state_alive_idle")]
public partial record Idle : Alive, IGet<Input.Attack>
{
namespace GameJamDungeon;
public virtual Transition On(in Input.Attack input)
{
GD.Print("Attacking...");
Output(new Output.Animations.Attack());
return To<Attacking>();
}
public partial class PlayerLogic
{
public abstract partial record State
{
[Meta, Id("player_logic_state_alive_idle")]
public partial record Idle : Alive, IGet<Input.Attack>
{
public virtual Transition On(in Input.Attack input)
{
GD.Print("Attacking...");
Output(new Output.Animations.Attack());
return To<Attacking>();
}
}
}

View File

@@ -1,48 +1,27 @@
using Chickensoft.Introspection;
using Godot;
namespace GameJamDungeon
namespace GameJamDungeon;
public partial class PlayerLogic
{
public partial class PlayerLogic
public partial record State
{
public partial record State
[Meta, Id("player_logic_alive")]
public abstract partial record Alive : State, IGet<Input.PhysicsTick>, IGet<Input.Killed>
{
[Meta, Id("player_logic_alive")]
public abstract partial record Alive : State, IGet<Input.PhysicsTick>, IGet<Input.Moved>, IGet<Input.Killed>
public virtual Transition On(in Input.PhysicsTick input)
{
public virtual Transition On(in Input.PhysicsTick input)
{
var delta = input.Delta;
var player = Get<IPlayer>();
var settings = Get<Settings>();
var delta = input.Delta;
var player = Get<IPlayer>();
player.Move((float)delta);
return ToSelf();
}
var rawInput = player.GetGlobalInputVector();
var strafeLeftInput = player.GetLeftStrafeInputVector();
var strafeRightInput = player.GetRightStrafeInputVector();
var transform = player.Transform;
transform.Basis = new Basis(Vector3.Up, settings.RotationSpeed * -rawInput.X * (float)delta) * transform.Basis;
var moveDirection = new Vector3(strafeRightInput - strafeLeftInput, 0, rawInput.Z);
var velocity = player.Basis * moveDirection * settings.MoveSpeed * settings.Acceleration;
Output(new Output.MovementComputed(transform.Basis, velocity));
return ToSelf();
}
public virtual Transition On(in Input.Moved input)
{
var gameRepo = Get<IGameRepo>();
gameRepo.SetPlayerGlobalPosition(input.GlobalPosition);
gameRepo.SetPlayerGlobalTransform(input.GlobalTransform);
return ToSelf();
}
public Transition On(in Input.Killed input)
{
GD.Print("Player died");
return To<Dead>();
}
public Transition On(in Input.Killed input)
{
GD.Print("Player died");
return To<Dead>();
}
}
}

View File

@@ -1,24 +1,23 @@
using Chickensoft.Introspection;
namespace GameJamDungeon
namespace GameJamDungeon;
public partial class PlayerLogic
{
public partial class PlayerLogic
public abstract partial record State
{
public abstract partial record State
[Meta, Id("player_logic_state_disabled")]
public partial record Disabled : State, IGet<Input.Enable>
{
[Meta, Id("player_logic_state_disabled")]
public partial record Disabled : State, IGet<Input.Enable>
public Disabled()
{
public Disabled()
{
OnAttach(() => Get<IAppRepo>().GameEntered += OnGameEntered);
OnDetach(() => Get<IAppRepo>().GameEntered -= OnGameEntered);
}
public Transition On(in Input.Enable input) => To<Idle>();
public void OnGameEntered() => Input(new Input.Enable());
OnAttach(() => Get<IAppRepo>().GameEntered += OnGameEntered);
OnDetach(() => Get<IAppRepo>().GameEntered -= OnGameEntered);
}
public Transition On(in Input.Enable input) => To<Idle>();
public void OnGameEntered() => Input(new Input.Enable());
}
}
}