Add door interaction system with requirements and HUD integration

This commit is contained in:
2025-10-30 00:57:28 +01:00
parent 9c0cd3f549
commit 5ae8b6f08c
21 changed files with 337 additions and 14 deletions

View File

@@ -7,6 +7,7 @@ using GameCore.Combat;
using GameCore.Combat.Interfaces;
using GameCore.ECS.Interfaces;
using GameCore.Input;
using GameCore.Interaction;
using GameCore.Inventory;
using GameCore.Movement;
using GameCore.Physics;
@@ -19,10 +20,12 @@ public class ComponentFactory
{
private readonly Dictionary<Type, Func<Resource, IComponent>> _factories = new();
private readonly EffectFactory _effectFactory;
private readonly InteractionRequirementFactory _requirementFactory;
public ComponentFactory(EffectFactory effectFactory)
public ComponentFactory(EffectFactory effectFactory, InteractionRequirementFactory requirementFactory)
{
_effectFactory = effectFactory;
_requirementFactory = requirementFactory;
Register<AttributeComponentResource>(CreateAttributeComponent);
Register<WeaponComponentResource>(CreateWeaponComponent);
@@ -36,6 +39,7 @@ public class ComponentFactory
Register<InventoryComponentResource>(_ => new InventoryComponent());
Register<PickupComponentResource>(CreatePickupComponent);
Register<EquipmentComponentResource>(_ => new EquipmentComponent());
Register<DoorComponentResource>(CreateDoorComponent);
}
public IComponent Create(Resource resource)
@@ -104,4 +108,25 @@ public class ComponentFactory
OnAcquireEffects = onAcquireEffects,
};
}
private DoorComponent CreateDoorComponent(DoorComponentResource resource)
{
var component = new DoorComponent
{
CurrentState = resource.InitialState,
IsOneTimeUnlock = resource.IsOneTimeUnlock,
OpenSpeed = resource.OpenSpeed,
};
foreach (var reqResource in resource.Requirements)
{
var requirement = _requirementFactory.Create(reqResource);
if (requirement != null)
{
component.Requirements.Add(requirement);
}
}
return component;
}
}

View File

@@ -0,0 +1,19 @@
using System;
using CryptonymThunder.Code.Resources;
using GameCore.Interaction;
using GameCore.Interaction.Interfaces;
namespace CryptonymThunder.Code.Factories;
public class InteractionRequirementFactory
{
public IInteractionRequirement Create(InteractionRequirementResource resource)
{
return resource switch
{
RequiresItemRequirementResource itemReq => new RequiresItemRequirement(itemReq.ItemId, itemReq.Quantity, itemReq.ConsumeItem),
RequiresAttributeRequirementResource attrReq => new RequiresAttributeRequirement(attrReq.Attribute, attrReq.RequiredValue, attrReq.Comparison),
_ => throw new ArgumentOutOfRangeException(nameof(resource), $"Requirement type {resource.GetType().Name} not recognized")
};
}
}

View File

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

View File

@@ -0,0 +1,54 @@
using GameCore.ECS;
using GameCore.ECS.Interfaces;
using GameCore.Interaction;
using Godot;
namespace CryptonymThunder.Code.Presenters;
[GlobalClass]
public partial class DoorPresenter : AnimatableBody3D, IEntityPresenter, IPresenterComponent
{
[Export] private AnimationPlayer _animationPlayer;
[Export] private string _openAnimationName = "Open";
private DoorComponent _doorComponent;
private Animation _openAnimation;
public Entity CoreEntity { get; set; }
public void Initialize(Entity coreEntity, World world)
{
CoreEntity = coreEntity;
_doorComponent = world.GetComponent<DoorComponent>(CoreEntity);
if (_animationPlayer == null)
{
world.Logger.Error($"DoorPresenter '{Name}' is missing an AnimationPlayer!");
return;
}
if (!_animationPlayer.HasAnimation(_openAnimationName))
{
world.Logger.Error($"DoorPresenter '{Name}' AnimationPlayer is missing animation: '{_openAnimationName}'");
return;
}
_openAnimation = _animationPlayer.GetAnimation(_openAnimationName);
_animationPlayer.Play(_openAnimationName);
_animationPlayer.Pause();
SyncToPresentation(0f);
}
public void SyncToPresentation(float delta)
{
if (_doorComponent == null || _openAnimation == null) return;
var targetTime = _doorComponent.OpenProgress * _openAnimation.Length;
_animationPlayer.Seek(targetTime, true);
}
public void SyncToCore(float delta)
{
}
}

View File

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

View File

@@ -11,6 +11,7 @@ using GameCore.ECS;
using GameCore.ECS.Interfaces;
using GameCore.Events;
using GameCore.Input;
using GameCore.Interaction;
using GameCore.Inventory;
using GameCore.Logging;
using GameCore.Logging.Interfaces;
@@ -27,6 +28,7 @@ public partial class GamePresenter : Node
[Export] private WeaponDatabase WeaponDatabase { get; set; }
[Export] private EntityArchetype PlayerArchetype { get; set; }
[Export] private SimulationConfigResource SimulationConfig { get; set; }
[Export(PropertyHint.Range, "1.0, 10.0, 0.1")] private float InteractionRange { get; set; } = 3.0f;
private World _world;
private PresenterRegistry _presenterRegistry;
@@ -61,7 +63,8 @@ public partial class GamePresenter : Node
_world = new World(_inputService, _worldQuery, simConfig, _logger);
var effectFactory = new EffectFactory();
var componentFactory = new ComponentFactory(effectFactory);
var requirementFactory = new InteractionRequirementFactory();
var componentFactory = new ComponentFactory(effectFactory, requirementFactory);
var weaponDataService = new GodotWeaponDataService(WeaponDatabase, effectFactory);
_presenterFactory = new PresenterFactory(_world, componentFactory, _presenterRegistry, this);
@@ -80,10 +83,14 @@ public partial class GamePresenter : Node
_world.RegisterSystem(new WeaponSwapSystem());
_world.RegisterSystem(new EquipmentSystem(_world, weaponDataService));
_world.RegisterSystem(new InteractionSystem(InteractionRange));
_world.RegisterSystem(new DoorSystem());
_world.RegisterSystem(new WeaponSystem());
_world.RegisterSystem(new ProjectileSystem());
_world.RegisterSystem(new ProjectileInitializationSystem(_world));
_world.RegisterSystem(new HealingSystem(_world));
_world.RegisterSystem(new DamageSystem(_world));
_world.RegisterSystem(new ProjectileCleanupSystem());

View File

@@ -5,6 +5,7 @@ using GameCore.Combat.Effects;
using GameCore.ECS;
using GameCore.ECS.Interfaces;
using GameCore.Events;
using GameCore.Interaction;
using GameCore.Inventory;
using GameCore.Player;
using Godot;
@@ -23,6 +24,7 @@ public partial class HudPresenterComponent : Control, IPresenterComponent
[Export] private Label _healthLabel;
[Export] private Label _ammoLabel;
[Export] private Label _weaponLabel;
[Export] private Label _interactLabel;
private string _currentAmmoId;
@@ -45,6 +47,11 @@ public partial class HudPresenterComponent : Control, IPresenterComponent
_world.Subscribe<EntityHealedEvent>(OnEntityHealed);
_world.Subscribe<InventoryItemChangedEvent>(OnInventoryChanged);
_world.Subscribe<WeaponEquippedEvent>(OnWeaponEquipped);
if (_interactLabel != null)
{
_interactLabel.Visible = false;
}
}
private void OnWeaponEquipped(WeaponEquippedEvent e)
@@ -113,6 +120,34 @@ public partial class HudPresenterComponent : Control, IPresenterComponent
public void SyncToPresentation(float delta)
{
if (_interactLabel != null)
{
var lookingAt = _world.GetComponent<IsLookingAtInteractableComponent>(_playerEntity);
if (lookingAt != null)
{
var door = _world.GetComponent<DoorComponent>(lookingAt.Target);
if (door != null)
{
var interactKey = "F";
_interactLabel.Text = door.CurrentState switch
{
DoorComponent.DoorState.Locked => $"[{interactKey}] Interact (Locked)",
DoorComponent.DoorState.Closed => $"[{interactKey}] Open Door",
DoorComponent.DoorState.Open => $"[{interactKey}] Close Door",
_ => ""
};
_interactLabel.Visible = !string.IsNullOrEmpty(_interactLabel.Text);
}
else
{
_interactLabel.Visible = false;
}
}
else
{
_interactLabel.Visible = false;
}
}
}
public void SyncToCore(float delta)

View File

@@ -0,0 +1,15 @@
using GameCore.Interaction;
using Godot;
using Godot.Collections;
namespace CryptonymThunder.Code.Resources;
[GlobalClass]
public partial class DoorComponentResource : Resource
{
[Export] public DoorComponent.DoorState InitialState { get; set; } = DoorComponent.DoorState.Closed;
[Export] public bool IsOneTimeUnlock { get; set; } = false;
[Export(PropertyHint.Range, "0.1,10.0,0.1")] public float OpenSpeed { get; set; } = 1.0f;
[ExportGroup("Requirements")] [Export] public Array<InteractionRequirementResource> Requirements { get; set; } = [];
}

View File

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

View File

@@ -0,0 +1,9 @@
using Godot;
namespace CryptonymThunder.Code.Resources;
[GlobalClass]
public partial class InteractionRequirementResource : Resource
{
}

View File

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

View File

@@ -0,0 +1,13 @@
using GameCore.Attributes;
using GameCore.Interaction;
using Godot;
namespace CryptonymThunder.Code.Resources;
[GlobalClass]
public partial class RequiresAttributeRequirementResource : InteractionRequirementResource
{
[Export] public Attribute Attribute { get; set; } = Attribute.Level;
[Export] public float RequiredValue { get; set; } = 1.0f;
[Export] public ComparisonType Comparison { get; set; } = ComparisonType.GreaterOrEqual;
}

View File

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

View File

@@ -0,0 +1,11 @@
using Godot;
namespace CryptonymThunder.Code.Resources;
[GlobalClass]
public partial class RequiresItemRequirementResource : InteractionRequirementResource
{
[Export] public string ItemId { get; set; } = "item_key_green";
[Export(PropertyHint.Range, "1,100,1")] public int Quantity { get; set; } = 1;
[Export] public bool ConsumeItem { get; set; } = true;
}

View File

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

View File

@@ -39,7 +39,7 @@ public class GodotInputService : IInputService
IsJumping = Input.IsActionJustPressed("jump");
IsFiring = Input.IsActionPressed("fire");
IsInteracting = Input.IsActionPressed("interact");
IsInteracting = Input.IsActionJustPressed("interact");
IsSwapWeaponNext = Input.IsActionJustPressed("swap_weapon_next");
IsSwapWeaponPrevious = Input.IsActionJustPressed("swap_weapon_previous");
}