469 lines
17 KiB
C#
469 lines
17 KiB
C#
![]() |
using System;
|
||
|
using System.Collections;
|
||
|
using System.Collections.Concurrent;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using System.Threading;
|
||
|
using BITKit;
|
||
|
using BITKit.Entities;
|
||
|
using BITKit.Mod;
|
||
|
using BITKit.StateMachine;
|
||
|
using BITKit.Tween;
|
||
|
using BITKit.UX;
|
||
|
using BITKit.UX.Hotkey;
|
||
|
using BITKit.WorldNode;
|
||
|
using Cysharp.Threading.Tasks;
|
||
|
using Microsoft.Extensions.DependencyInjection;
|
||
|
using Net.Project.B.Damage;
|
||
|
using Net.Project.B.Emoji;
|
||
|
using Net.Project.B.Health;
|
||
|
using Net.Project.B.Interaction;
|
||
|
using Net.Project.B.Inventory;
|
||
|
using Net.Project.B.Item;
|
||
|
using Net.Project.B.UX;
|
||
|
using Project.B.CharacterController;
|
||
|
using Project.B.Entities;
|
||
|
using Project.B.Item;
|
||
|
using Project.B.Map;
|
||
|
using Project.B.Player;
|
||
|
using Unity.Mathematics;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.InputSystem;
|
||
|
using UnityEngine.InputSystem.Interactions;
|
||
|
using UnityEngine.UIElements;
|
||
|
|
||
|
namespace Project.B.UX
|
||
|
{
|
||
|
public class UXHUD : UIToolKitPanel,IUXHud
|
||
|
{
|
||
|
protected override string DocumentPath => "ux_hud";
|
||
|
public override bool AllowCursor => false;
|
||
|
public override bool AllowInput => true;
|
||
|
private readonly IDamageService _damageService;
|
||
|
private readonly IEntitiesService _entitiesService;
|
||
|
private readonly IManagedItemService _itemService;
|
||
|
private readonly IGameMapService _gameMapService;
|
||
|
private readonly IWorldInteractionService _interactionService;
|
||
|
private readonly IUXKeyMap<InputAction> _uxKeyMap;
|
||
|
private readonly ConcurrentDictionary<int, VisualElement> _prompts = new();
|
||
|
private readonly IPlayerFactory _playerFactory;
|
||
|
private readonly IKnockedService _knockedService;
|
||
|
private readonly IUXItemInspector _itemInspector;
|
||
|
private readonly ValidHandle _isVisible = new();
|
||
|
private readonly IPlayerKeyMap<InputAction> _playerKeyMap;
|
||
|
private readonly IntervalUpdate _fpsSampleInterval=new(0.32f);
|
||
|
private readonly IMicroStateMachine<IPlayerControlMode> _controlMode;
|
||
|
private readonly IUXHotKey _uxHotKey;
|
||
|
private IEntity _entity;
|
||
|
public UXHUD(IUXService uxService, IGameMapService gameMapService, IUXKeyMap<InputAction> uxKeyMap, IWorldInteractionService interactionService, IPlayerFactory playerFactory, IKnockedService knockedService, IManagedItemService itemService, IPlayerKeyMap<InputAction> playerKeyMap, IUXItemInspector itemInspector, IMicroStateMachine<IPlayerControlMode> controlMode, IEntitiesService entitiesService, IDamageService damageService, IUXHotKey uxHotKey) : base(uxService)
|
||
|
{
|
||
|
_gameMapService = gameMapService;
|
||
|
_uxKeyMap = uxKeyMap;
|
||
|
_interactionService = interactionService;
|
||
|
_playerFactory = playerFactory;
|
||
|
_knockedService = knockedService;
|
||
|
_itemService = itemService;
|
||
|
_playerKeyMap = playerKeyMap;
|
||
|
_itemInspector = itemInspector;
|
||
|
_controlMode = controlMode;
|
||
|
_entitiesService = entitiesService;
|
||
|
_damageService = damageService;
|
||
|
_uxHotKey = uxHotKey;
|
||
|
_gameMapService.OnMapChanged += OnMapChanged;
|
||
|
|
||
|
_interactionService.OnInteraction += OnInteraction;
|
||
|
|
||
|
|
||
|
_playerFactory.OnEntityCreated += OnEntityCreated;
|
||
|
|
||
|
_knockedService.OnKnocked += OnKnocked;
|
||
|
_knockedService.OnKnockedHealthChanged += OnKnockedHealthChanged;
|
||
|
|
||
|
OnInitiatedAsync += InitiatedAsync;
|
||
|
|
||
|
|
||
|
_controlMode.OnStateChanged += OnControlModeChanged;
|
||
|
|
||
|
_damageService.OnDamaged += OnDamaged;
|
||
|
}
|
||
|
|
||
|
private async UniTask InitiatedAsync()
|
||
|
{
|
||
|
_isVisible.AddListener(RootVisualElement.SetActive);
|
||
|
_isVisible.Invoke();
|
||
|
|
||
|
_healthBar.value = 100;
|
||
|
|
||
|
_healthBar.SetActive(true);
|
||
|
_knockedBar.SetActive(false);
|
||
|
|
||
|
_promptTemplate = await ModService.LoadAsset<VisualTreeAsset>("ux_prompt_template");
|
||
|
_itemTemplate = await ModService.LoadAsset<VisualTreeAsset>("ux_item_template");
|
||
|
|
||
|
_flatItemTemplate = await ModService.LoadAsset<VisualTreeAsset>("ux_item_template-flat");
|
||
|
|
||
|
_damageTemplate = await ModService.LoadAsset<VisualTreeAsset>("ui_damage-template");
|
||
|
|
||
|
_promptsContainer.Clear();
|
||
|
|
||
|
_hitMark.SetOpacity(0);
|
||
|
}
|
||
|
|
||
|
private CancellationTokenSource _hitMarkCts;
|
||
|
private async void OnDamaged(IDamageReport obj)
|
||
|
{
|
||
|
if (obj.IsFatal)
|
||
|
{
|
||
|
if(_entitiesService.TryGetEntity(obj.Initiator,out var e) && e.ServiceProvider.GetRequiredService<IdComponent>() is {} initiator)
|
||
|
{
|
||
|
if (_entitiesService.TryGetEntity(obj.Target, out var t) &&
|
||
|
t.ServiceProvider.GetRequiredService<IdComponent>() is { } target)
|
||
|
{
|
||
|
|
||
|
if (_damageContainer.childCount > 5)
|
||
|
{
|
||
|
_damageContainer.Children().Last().RemoveFromHierarchy();
|
||
|
}
|
||
|
|
||
|
var container = _damageContainer.Create(_damageTemplate);
|
||
|
|
||
|
if (_playerFactory.Entities.ContainsKey(obj.Initiator))
|
||
|
{
|
||
|
container.AddToClassList("selected");
|
||
|
}
|
||
|
|
||
|
if (obj.DamageType is ScriptableItemDamage { ScriptableId: not 0 } scriptableItemDamage)
|
||
|
{
|
||
|
var item =await UXInventoryUtils.GetScriptableItem(scriptableItemDamage.ScriptableId);
|
||
|
container.Get<VisualElement>().style.backgroundImage = new(item.Icon);
|
||
|
}
|
||
|
|
||
|
container.Get<Label>().text = initiator.Name;
|
||
|
container.Get<Label>(1).text = target.Name;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
if (_entity is not null && obj.Initiator == _entity.Id && obj.Initiator!=obj.Target)
|
||
|
{
|
||
|
_hitMarkCts?.Cancel();
|
||
|
_hitMarkCts = new();
|
||
|
|
||
|
try
|
||
|
{
|
||
|
_hitMark.style.unityBackgroundImageTintColor = obj.IsFatal ? Color.red : Color.white;
|
||
|
|
||
|
_hitMark.SetOpacity(1);
|
||
|
|
||
|
float3 scale = obj.IsFatal ? new(2, 2, 2) : new(1.2f, 1.2f, 1.2f);
|
||
|
|
||
|
await BITween.Lerp(x => _hitMark.transform.scale = x, float3.zero, scale, 0.1f,
|
||
|
math.lerp, _hitMarkCts.Token);
|
||
|
|
||
|
await BITween.Lerp(x => _hitMark.transform.scale = x, scale, new float3(1f, 1f, 1f), 0.1f,
|
||
|
math.lerp, _hitMarkCts.Token);
|
||
|
|
||
|
await UniTask.Delay(obj.IsFatal ? 1000 : 200).AttachExternalCancellation(_hitMarkCts.Token);
|
||
|
|
||
|
await BITween.Lerp(_hitMark.SetOpacity, 1f, 0, 0.2f,Mathf.Lerp, _hitMarkCts.Token);
|
||
|
}
|
||
|
catch (OperationCanceledException)
|
||
|
{
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void OnControlModeChanged(IPlayerControlMode arg1, IPlayerControlMode arg2)
|
||
|
{
|
||
|
_isVisible.SetDisableElements(_controlMode,arg2 switch
|
||
|
{
|
||
|
PlayerSpotterScopeMode=>true,
|
||
|
_=>false,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
[UXBindPath("prompts-container")]
|
||
|
private VisualElement _promptsContainer;
|
||
|
[UXBindPath("equipSelector-container")]
|
||
|
private VisualElement _equipSelectorContainer;
|
||
|
[UXBindPath("health-bar")]
|
||
|
private ProgressBar _healthBar;
|
||
|
[UXBindPath("knocked-bar")]
|
||
|
private ProgressBar _knockedBar;
|
||
|
[UXBindPath("cross-hair")]
|
||
|
private VisualElement _crosshair;
|
||
|
[UXBindPath("ammo-label")]
|
||
|
private Label _ammoLabel;
|
||
|
[UXBindPath("fps-label")]
|
||
|
private Label _fpsLabel;
|
||
|
[UXBindPath("crosshair-image")]
|
||
|
private VisualElement _crossHairImage;
|
||
|
[UXBindPath("equipment-container")]
|
||
|
private VisualElement _equipContainer;
|
||
|
[UXBindPath("stamina-bar")]
|
||
|
private ProgressBar _staminaBar;
|
||
|
|
||
|
[UXBindPath("damage-container")]
|
||
|
private VisualElement _damageContainer;
|
||
|
|
||
|
[UXBindPath("hitmarker-image")]
|
||
|
private VisualElement _hitMark;
|
||
|
|
||
|
private VisualTreeAsset _promptTemplate;
|
||
|
private VisualTreeAsset _itemTemplate;
|
||
|
private VisualTreeAsset _flatItemTemplate;
|
||
|
private VisualTreeAsset _damageTemplate;
|
||
|
|
||
|
[Inject] private IHealthComponent _healthComponent;
|
||
|
[Inject]
|
||
|
private IPlayerWeaponInventory _weaponInventory;
|
||
|
[Inject]
|
||
|
private IPlayerInventory _playerInventory;
|
||
|
[Inject]
|
||
|
private IPlayerCharacterController _playerCharacterController;
|
||
|
[Inject]
|
||
|
private IPlayerEquipmentInventory _equipmentInventory;
|
||
|
[Inject]
|
||
|
private IEmojiService<AnimationClip> _emojiService;
|
||
|
|
||
|
|
||
|
private int _ammoId;
|
||
|
private int _currentAmmo;
|
||
|
private int _remainingAmmo;
|
||
|
|
||
|
private readonly ConcurrentDictionary<int, VisualElement> _equipmentDictionary = new();
|
||
|
|
||
|
private void OnKnockedHealthChanged(int arg1, int arg2, int arg3)
|
||
|
{
|
||
|
if(_entity.Id!=arg1)return;
|
||
|
_knockedBar.value = Mathf.Clamp(arg3, 0, _knockedBar.highValue);
|
||
|
}
|
||
|
|
||
|
private void OnKnocked(int arg1, bool arg2)
|
||
|
{
|
||
|
if(_entity.Id!=arg1)return;
|
||
|
_healthBar.SetActive(!arg2);
|
||
|
_knockedBar.SetActive(arg2);
|
||
|
_knockedBar.value = 100;
|
||
|
}
|
||
|
|
||
|
private UniTask OnEntityCreated(string arg1, IEntity arg2)
|
||
|
{
|
||
|
arg2.Inject(this);
|
||
|
_isVisible.AddElement(arg2);
|
||
|
arg2.CancellationToken.Register(() => _isVisible.RemoveElement(arg2));
|
||
|
if (_healthBar is not null)
|
||
|
{
|
||
|
_healthBar.value = 100;
|
||
|
}
|
||
|
|
||
|
_entity = arg2;
|
||
|
|
||
|
_weaponInventory.StateMachine.OnStateChanged += OnStateChanged;
|
||
|
|
||
|
_itemService.OnItemAddedOrUpdated += OnItemAddedOrUpdated;
|
||
|
|
||
|
_playerInventory.Container.OnRelease += OnRelease;
|
||
|
|
||
|
_equipmentInventory.OnItemAdded += OnItemAdded;
|
||
|
_equipmentInventory.OnItemRemoved += OnItemRemoved;
|
||
|
_equipmentInventory.OnItemConsumed += OnItemRemoved;
|
||
|
|
||
|
OnStateChanged(null,_weaponInventory.StateMachine.CurrentState);
|
||
|
|
||
|
_equipContainer.Clear();
|
||
|
|
||
|
_playerCharacterController.OnStaminaChanged += OnStaminaChanged;
|
||
|
|
||
|
_damageContainer.Clear();
|
||
|
|
||
|
_healthComponent.OnHealthChanged += OnHealthChanged;
|
||
|
|
||
|
return UniTask.CompletedTask;
|
||
|
}
|
||
|
|
||
|
private void OnStaminaChanged(int arg1, int arg2)
|
||
|
{
|
||
|
_staminaBar.SetActive(arg2<100);
|
||
|
_staminaBar.value = arg2*0.01f;
|
||
|
}
|
||
|
|
||
|
private void OnItemRemoved(int arg1, IRuntimeItem arg2)
|
||
|
{
|
||
|
if(_equipmentDictionary.TryGetValue(arg1,out var visualElement) is false)return;
|
||
|
visualElement.RemoveFromHierarchy();
|
||
|
}
|
||
|
|
||
|
private void OnItemAdded(int arg1, IRuntimeItem arg2)
|
||
|
{
|
||
|
if(_equipmentDictionary.ContainsKey(arg1))return;
|
||
|
|
||
|
var visualElement = _equipContainer.Create(_flatItemTemplate);
|
||
|
UXInventoryUtils.Setup(visualElement,arg2).Forget();
|
||
|
|
||
|
_equipmentDictionary.TryAdd(arg1, visualElement);
|
||
|
}
|
||
|
|
||
|
private void OnRelease(bool obj)
|
||
|
{
|
||
|
if (_weaponId is 0) return;
|
||
|
_remainingAmmo = _playerInventory.Container.GetItems().Where(x=>x.ScriptableId==_ammoId).Sum(x => x.Amount);
|
||
|
_ammoLabel.text = $"{_currentAmmo}/{_remainingAmmo}";
|
||
|
}
|
||
|
|
||
|
private void OnItemAddedOrUpdated(IRuntimeItem obj)
|
||
|
{
|
||
|
if (obj.Id == _weaponId)
|
||
|
{
|
||
|
_currentAmmo = obj.Amount;
|
||
|
_ammoLabel.text = $"{_currentAmmo}/{_remainingAmmo}";
|
||
|
}
|
||
|
}
|
||
|
private int _weaponId;
|
||
|
private async void OnStateChanged(IPlayerWeaponController arg1, IPlayerWeaponController arg2)
|
||
|
{
|
||
|
_weaponId = 0;
|
||
|
_equipSelectorContainer.Clear();
|
||
|
if(arg2 is null)return;
|
||
|
if(_itemService.Items.TryGetValue(arg2.Identifier,out var item) is false)return;
|
||
|
var container = _equipSelectorContainer.Create(_itemTemplate);
|
||
|
UXInventoryUtils.Setup(container,item).Forget();
|
||
|
_weaponId = arg2.Identifier;
|
||
|
if (await ModServiceDictionaryReferenceExtensions.LoadAssets<ScriptableItem>(item.ScriptableId) is ScriptableGun
|
||
|
scriptableGun)
|
||
|
{
|
||
|
_ammoId = scriptableGun.DefaultAmmoType.Id;
|
||
|
_currentAmmo = item.Amount;
|
||
|
OnRelease(true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void OnHealthChanged(int prevHp, int newHp)
|
||
|
{
|
||
|
_healthBar.value = Mathf.Clamp(newHp, 0, _healthBar.highValue);
|
||
|
}
|
||
|
|
||
|
public override async void OnTick(float deltaTime)
|
||
|
{
|
||
|
base.OnTick(deltaTime);
|
||
|
if (_fpsSampleInterval.AllowUpdate)
|
||
|
{
|
||
|
_fpsLabel.text = $"FPS:{((int)(1f / deltaTime)).ToString()}";
|
||
|
}
|
||
|
|
||
|
if (_playerCharacterController is not null)
|
||
|
{
|
||
|
|
||
|
var opacity = _playerCharacterController switch
|
||
|
{
|
||
|
{ } x when x.ZoomFactor.Values.Max() is not 1 => 0,
|
||
|
{ CharacterController: { CurrentState: ICharacterSeating } } => 0,
|
||
|
_ => 1
|
||
|
};
|
||
|
_crosshair.SetOpacity(opacity);
|
||
|
|
||
|
var startPos = (Vector3)_playerCharacterController.CharacterController.ViewPosition;
|
||
|
var endPos =startPos +(Quaternion)_playerCharacterController.CharacterController.ViewRotation * Vector3.forward *
|
||
|
1;
|
||
|
|
||
|
_crossHairImage.SetPosition(endPos );
|
||
|
}
|
||
|
|
||
|
|
||
|
if (Keyboard.current is { f1Key: { wasPressedThisFrame: true } })
|
||
|
{
|
||
|
var collection = new HotkeyCollection();
|
||
|
|
||
|
foreach (var emojiData in await _emojiService.GetAllEmojis())
|
||
|
{
|
||
|
collection.Register(new HotkeyProvider()
|
||
|
{
|
||
|
Name = emojiData.Name,
|
||
|
Description = emojiData.Description,
|
||
|
Enabled = true,
|
||
|
OnPerform = ()=>_emojiService.Play(emojiData),
|
||
|
});
|
||
|
}
|
||
|
_uxHotKey.Perform(collection);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void OnInteraction(object arg1, IWorldInteractable arg2, WorldInteractionProcess arg3, object arg4)
|
||
|
{
|
||
|
|
||
|
var id = arg2.WorldObject.As<GameObject>().GetInstanceID();;
|
||
|
switch (arg3)
|
||
|
{
|
||
|
case WorldInteractionProcess.Hover:
|
||
|
{
|
||
|
if (_prompts.ContainsKey(id)) return;
|
||
|
var visualElement = _promptsContainer.Create(_promptTemplate);
|
||
|
|
||
|
_prompts.TryAdd(id, visualElement);
|
||
|
|
||
|
if (_entitiesService.TryGetEntity(id, out var entity) &&
|
||
|
entity.ServiceProvider.GetService<WorldInfoNode>() is { } infoNode)
|
||
|
{
|
||
|
|
||
|
visualElement.Get<Label>(1).text = infoNode.Name;
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case WorldInteractionProcess.None:
|
||
|
{
|
||
|
if (_prompts.TryRemove(id, out var visualElement) is false) return;
|
||
|
visualElement.RemoveFromHierarchy();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
protected override void OnPanelEntry()
|
||
|
{
|
||
|
base.OnPanelEntry();
|
||
|
InputActionGroup.RegisterCallback(_uxKeyMap.CancelKey, OnCancel);
|
||
|
InputActionGroup.RegisterCallback(_uxKeyMap.InventoryKey , OnInventory);
|
||
|
InputActionGroup.RegisterCallback(_playerKeyMap.InspectKey, OnInspect);
|
||
|
|
||
|
}
|
||
|
|
||
|
private void OnInspect(InputAction.CallbackContext obj)
|
||
|
{
|
||
|
if(obj is not{interaction:HoldInteraction,performed:true})return;
|
||
|
if(_weaponInventory.StateMachine.CurrentState is not {} currentState)return;
|
||
|
_itemInspector.Inspect(_itemService.Items[currentState.Identifier]);
|
||
|
}
|
||
|
|
||
|
private void OnInventory(InputAction.CallbackContext obj)
|
||
|
{
|
||
|
UXService.Entry<IUXInventory>();
|
||
|
}
|
||
|
|
||
|
protected override void OnPanelExit()
|
||
|
{
|
||
|
base.OnPanelExit();
|
||
|
InputActionGroup.UnRegisterCallback(_uxKeyMap.CancelKey,
|
||
|
OnCancel);
|
||
|
InputActionGroup.UnRegisterCallback(_uxKeyMap.InventoryKey,
|
||
|
OnInventory);
|
||
|
InputActionGroup.UnRegisterCallback(_playerKeyMap.InspectKey, OnInspect);
|
||
|
}
|
||
|
|
||
|
private void OnCancel(InputAction.CallbackContext obj)
|
||
|
{
|
||
|
if(obj is not{interaction:PressInteraction,performed:true}) return;
|
||
|
UXService.Entry<IUXLobby>();
|
||
|
}
|
||
|
private void OnMapChanged(Guid playerId,string obj)
|
||
|
{
|
||
|
if (string.IsNullOrEmpty(obj) is false)
|
||
|
{
|
||
|
UXService.Entry(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public ValidHandle InCinematicMode { get; } = new();
|
||
|
}
|
||
|
}
|