This commit is contained in:
2026-03-02 22:28:23 -08:00
parent 701e7b0858
commit 7b6ca68e65
15 changed files with 164 additions and 26 deletions

View File

@@ -0,0 +1,13 @@
using Chickensoft.Collections;
using Godot;
using Zennysoft.Ma.Adapter;
public interface IStatusEffectComponent : IEntityComponent
{
[Export] public double RustDuration { get; set; }
public AutoProp<bool> Rust { get; }
public bool ImmuneToRust { get; set; }
}

View File

@@ -31,6 +31,8 @@ namespace Zennysoft.Ma.Adapter.Entity
public IDefenseComponent DefenseComponent { get; }
public IStatusEffectComponent StatusEffectComponent { get; }
public ElementalResistanceSet ElementalResistanceSet { get; }
public int InitialHP { get; }

View File

@@ -18,12 +18,6 @@ public interface IGameRepo : IDisposable
event Action<IBaseInventoryItem>? RemoveItemFromInventoryEvent;
event Action? PlayerAttack;
event Action? PlayerAttackedWall;
event Action? PlayerAttackedEnemy;
event Action<IEquipableItem>? EquippedItem;
event Action<IEquipableItem>? UnequippedItem;
@@ -42,10 +36,6 @@ public interface IGameRepo : IDisposable
public void RemoveItemFromInventory(IBaseInventoryItem item);
public void OnPlayerAttack();
public void OnPlayerAttackedWall();
public void CloseInventory();
public void GameEnded();
@@ -64,9 +54,6 @@ public class GameRepo : IGameRepo
public event Action<string>? AnnounceMessageOnMainScreenEvent;
public event Action<string>? AnnounceMessageInInventoryEvent;
public event Action<IBaseInventoryItem>? RemoveItemFromInventoryEvent;
public event Action? PlayerAttack;
public event Action? PlayerAttackedWall;
public event Action? PlayerAttackedEnemy;
public event Action<IEquipableItem>? EquippedItem;
public event Action<IEquipableItem>? UnequippedItem;
public event Action<IEnemy>? EnemyDied;
@@ -107,16 +94,6 @@ public class GameRepo : IGameRepo
RemoveItemFromInventoryEvent?.Invoke(item);
}
public void OnPlayerAttack()
{
PlayerAttack?.Invoke();
}
public void OnPlayerAttackedWall()
{
PlayerAttackedWall?.Invoke();
}
public void CloseInventory()
{
CloseInventoryEvent?.Invoke();

View File

@@ -46,6 +46,8 @@ public interface IPlayer : IKillable, ICharacterBody3D
public IEquipmentComponent EquipmentComponent { get; }
public IStatusEffectComponent StatusEffectComponent { get; }
public void SetHealthTimerStatus(bool isActive);
public void ModifyHealthTimerSpeed(float newModifier);

View File

@@ -0,0 +1,20 @@
using Chickensoft.Collections;
public class StatusEffectComponent : IStatusEffectComponent
{
public StatusEffectComponent(double rustDuration)
{
RustDuration = rustDuration;
}
public double RustDuration { get; set; }
public AutoProp<bool> Rust { get; } = new AutoProp<bool>(false);
public bool ImmuneToRust { get; set; } = false;
public void Reset()
{
Rust.OnNext(false);
}
}

View File

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

View File

@@ -34,6 +34,8 @@ public abstract partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLo
public IDefenseComponent DefenseComponent { get; private set; }
public IStatusEffectComponent StatusEffectComponent { get; private set; }
public virtual IEnemyModelView EnemyModelView { get; set; } = default!;
public Vector3 TargetPosition { get; private set; }
@@ -69,6 +71,9 @@ public abstract partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLo
[Node] private AudioStreamPlayer3D _dieSFX { get; set; } = default!;
[Node] private AudioStreamPlayer3D _aggroSFX { get; set; } = default!;
private Timer _rustTimer;
private Timer _rustDuration;
protected bool _activated = false;
private Vector3 _previousPosition = Vector3.Zero;
@@ -86,8 +91,22 @@ public abstract partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLo
HealthComponent.HealthReachedZero += Die;
HealthComponent.DamageTaken += TakeHit;
_rustTimer = new Timer();
_rustDuration = new Timer();
_rustTimer.WaitTime = 3;
_rustDuration.WaitTime = 30;
_rustTimer.Timeout += _rustTimer_Timeout;
_rustDuration.Timeout += _rustDuration_Timeout;
AddChild(_rustTimer);
AddChild(_rustDuration);
AttackComponent = new AttackComponent(InitialAttack);
DefenseComponent = new DefenseComponent(InitialDefense);
StatusEffectComponent = new StatusEffectComponent(30);
StatusEffectComponent.Rust.Changed += OnRusted;
EnemyBinding
.Handle((in EnemyLogic.Output.Activate _) =>
@@ -160,6 +179,8 @@ public abstract partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLo
public virtual void Die()
{
SetPhysicsProcess(false);
_rustDuration.Stop();
_rustTimer.Stop();
_enemyLogic.Input(new EnemyLogic.Input.Defeated());
_player.ExperiencePointsComponent.Gain(ExpGiven);
EnemyModelView.PlayDeathAnimation();
@@ -244,4 +265,28 @@ public abstract partial class Enemy : CharacterBody3D, IEnemy, IProvide<IEnemyLo
{
_player.TakeDamage(new AttackData(AttackComponent.CurrentAttack.Value, ElementType.None));
}
private void OnRusted(bool rustStatus)
{
if (rustStatus)
{
_rustTimer.Start();
_rustDuration.Start();
}
else
{
_rustTimer.Stop();
}
}
private void _rustTimer_Timeout()
{
HealthComponent.Damage(3);
TakeHit();
}
private void _rustDuration_Timeout()
{
StatusEffectComponent.Rust.OnNext(false);
}
}

View File

@@ -80,6 +80,9 @@ public partial class Game : Node3D, IGame
private Timer _doubleExpTimer;
private Timer _rustTimer;
private Timer _rustDurationTimer;
[Signal] private delegate void OnLoadLevelRequestEventHandler();
public event Action<string> InventoryEventNotification;
@@ -191,6 +194,19 @@ public partial class Game : Node3D, IGame
_doubleExpTimer.Timeout += EndDoubleExpTimer;
AddChild(_doubleExpTimer);
_player.StatusEffectComponent.Rust.Changed += RustStatusChanged;
_rustTimer = new Timer();
_rustTimer.WaitTime = 2f;
_rustTimer.Timeout += RustTimeout;
_rustDurationTimer = new Timer();
_rustDurationTimer.WaitTime = _player.StatusEffectComponent.RustDuration;
_rustDurationTimer.Timeout += RustWoreOff;
AddChild(_rustTimer);
AddChild(_rustDurationTimer);
GameRepo.IsPaused.Sync += IsPaused_Sync;
InGameUI.PlayerInfoUI.Activate();
InGameUI.Show();
@@ -414,16 +430,16 @@ public partial class Game : Node3D, IGame
{
var restorativeScene = GD.Load<PackedScene>("res://src/items/restorative/Restorative.tscn");
var restorative = restorativeScene.Instantiate<Restorative>();
restorative.GlobalPosition = new Vector3(vector.X, 2f, vector.Z) + (-_player.GetGlobalBasis().Z);
AddChild(restorative);
restorative.GlobalPosition = new Vector3(vector.X, 2f, vector.Z) + (-_player.GetGlobalBasis().Z);
}
private void DropItem(Vector3 vector)
{
var randomItem = ItemDatabase.Instance.PickItem<IBaseInventoryItem>() as Node3D;
var duplicated = randomItem.Duplicate((int)DuplicateFlags.UseInstantiation) as Node3D;
duplicated.GlobalPosition = new Vector3(vector.X, 2f, vector.Z) + (-_player.GetGlobalBasis().Z);
AddChild(duplicated);
duplicated.GlobalPosition = new Vector3(vector.X, 2f, vector.Z) + (-_player.GetGlobalBasis().Z);
}
private void UseTeleportPrompt_CloseTeleportPrompt()
@@ -576,6 +592,12 @@ public partial class Game : Node3D, IGame
SfxDatabase.Instance.Play(SoundEffect.HealVT);
InventoryEventNotification.Invoke($"Restored {consumableItem.HealVTAmount} VT.");
}
if (consumableItem.Stats.HealsStatusAilments)
{
_player.StatusEffectComponent.Reset();
InventoryEventNotification.Invoke($"All status afflictments have faded.");
}
}
private void EnactEffectItemEffects(EffectItem effectItem)
@@ -809,6 +831,29 @@ public partial class Game : Node3D, IGame
_player.Activate();
}
private void RustStatusChanged(bool rustStatus)
{
if (rustStatus)
{
_rustTimer.Start();
_rustDurationTimer.Start();
GameRepo.AnnounceMessageOnMainScreen("Afflicted with Rust.");
}
else
{
_rustTimer.Stop();
GameRepo.AnnounceMessageOnMainScreen("Rust affliction has faded.");
}
}
private void RustTimeout()
{
_player.HealthComponent.Damage(3);
}
private void RustWoreOff() => _player.StatusEffectComponent.Rust.OnNext(false);
public void NotifyInventory(string message) => InventoryEventNotification?.Invoke(message);
private void OnQuit() => GameExitRequested?.Invoke();

View File

@@ -21,4 +21,7 @@ public partial class ConsumableItemStats : InventoryItemStats
[Export]
[Save("consumable_item_raise_vt")]
public int PermanentRaiseVTAmount { get; set; } = 0;
[Export]
public bool HealsStatusAilments { get; set; } = false;
}

View File

@@ -9,6 +9,7 @@ HealHPAmount = 0
HealVTAmount = 0
PermanentRaiseHPAmount = 0
PermanentRaiseVTAmount = 0
HealsStatusAilments = true
Name = "Ancient Capsule"
StatDescription = "Heals all status ailments."
FlavorText = ""

View File

@@ -8,7 +8,7 @@ height = 0.725098
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_wll7p"]
radius = 0.470016
[node name="Ammo" type="RigidBody3D"]
[node name="Weapon" type="RigidBody3D"]
collision_layer = 0
axis_lock_linear_x = true
axis_lock_linear_z = true

View File

@@ -125,6 +125,11 @@ unique_name_in_owner = true
layout_mode = 2
text = "Die"
[node name="RustButton" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer/VFlowContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Rust"
[node name="DebugInfoCheckbox" type="CheckBox" parent="MarginContainer/VBoxContainer/HBoxContainer/VFlowContainer"]
unique_name_in_owner = true
layout_mode = 2

View File

@@ -21,6 +21,7 @@ public partial class DummyPlayer : CharacterBody3D, IPlayer
public int HealthTimerHPRate { get; set; }
public float HealthTimerSpeedModifier { get; }
public bool AutoIdentifyItems { get; set; }
public IStatusEffectComponent StatusEffectComponent { get; }
public event Action PlayerDied;

View File

@@ -37,6 +37,8 @@ public partial class Player : CharacterBody3D, IPlayer, IProvide<IPlayer>
public IEquipmentComponent EquipmentComponent { get; private set; }
public IStatusEffectComponent StatusEffectComponent { get; private set; }
public Vector3 CurrentPosition => GlobalPosition;
public Basis CurrentBasis => Transform.Basis;
@@ -70,6 +72,8 @@ public partial class Player : CharacterBody3D, IPlayer, IProvide<IPlayer>
[Export(PropertyHint.Range, "1, 100, 1")] public int InitialLuck { get; set; } = 8;
[Export(PropertyHint.Range, "1, 60, 1")] public double RustDuration { get; set; } = 30;
[Export]
private bool HealthTimerIsActive = false;
#endregion
@@ -149,6 +153,7 @@ public partial class Player : CharacterBody3D, IPlayer, IProvide<IPlayer>
ExperiencePointsComponent = new ExperiencePointsComponent();
LuckComponent = new LuckComponent(InitialLuck);
EquipmentComponent = new EquipmentComponent();
StatusEffectComponent = new StatusEffectComponent(RustDuration);
_itemReroller = new ItemReroller(ItemDatabase.Instance);
_playerEffectService = new PlayerEffectService(this);
@@ -872,6 +877,19 @@ public partial class Player : CharacterBody3D, IPlayer, IProvide<IPlayer>
_playerEffectService.TakeSelfDamage(weapon.Stats.SelfDamage);
if (weapon.WeaponTag == WeaponTag.Instakill)
_playerEffectService.Instakill(enemy);
if (weapon.WeaponTag == WeaponTag.RustChanceSelfAndEnemy)
{
var rustChance = 0.15f;
var rng = new RandomNumberGenerator();
rng.Randomize();
if (rng.Randf() <= rustChance)
{
if (rng.Randf() >= 0.5f && ((Accessory)EquipmentComponent.EquippedAccessory.Value).AccessoryTag != AccessoryTag.StatusEffectImmunity)
StatusEffectComponent.Rust.OnNext(true);
else
enemy.StatusEffectComponent.Rust.OnNext(true);
}
}
}
private async void CollisionDetector_AreaEntered(Area3D area)

View File

@@ -26,6 +26,8 @@ public partial class PauseDebugMenu : Control, IDebugMenu
[Node] public Button LoadNextFloorButton { get; set; } = default!;
[Node] public Button RustButton { get; set; } = default!;
[Node] public Button DieButton { get; set; } = default!;
[Node] public CheckBox DebugInfoCheckbox { get; set; } = default!;
@@ -42,6 +44,7 @@ public partial class PauseDebugMenu : Control, IDebugMenu
{
LoadNextFloorButton.Pressed += LoadNextFloorButton_Pressed;
DieButton.Pressed += DieButton_Pressed;
RustButton.Pressed += RustButton_Pressed;
_itemDatabase = ItemDatabase.Instance;
_spawnableItems = _itemDatabase.Items;
@@ -94,6 +97,8 @@ public partial class PauseDebugMenu : Control, IDebugMenu
private void DieButton_Pressed() => _player.Die();
private void RustButton_Pressed() => _player.StatusEffectComponent.Rust.OnNext(true);
private void FloorSelectDropDown_ItemSelected(long index)
{
var sceneName = FloorSelectDropDown.GetItemText((int)index);