(client) feat:实现掉落物,UI显示,维度

This commit is contained in:
m0_75251201
2025-08-27 13:56:22 +08:00
parent f04c89046b
commit 0c99e2beee
46 changed files with 6150 additions and 1809 deletions

View File

@ -30,7 +30,7 @@ namespace Entity
private void OnTriggerEnter2D(Collider2D other)
{
var entity = other.GetComponent<Entity>();
if (!entity || entity == bulletSource) return;
if (!entity || entity == bulletSource || entity is Pickup) return;
if (Managers.AffiliationManager.Instance.GetRelation(bulletSource.affiliation, entity.affiliation) != Relation.Friendly)
{
entity.OnHit(this);

View File

@ -1,89 +1,110 @@
using System;
using System.Linq;
using Data;
using Item;
using Managers;
using UnityEngine;
using Utils;
// 添加 System 命名空间以使用 Action
namespace Entity
{
public class Character : Entity
{
// ------------- 新增 / 修改部分 START -------------
private int _currentSelected; // 私有字段用于存储实际值
// 声明 Inventory 字段。更改为 private set 以确保 Inventory 实例只能在 Character 内部设置。
public Inventory Inventory { get; private set; }
/// <summary>
/// 当前选中的背包槽位索引。
/// 当此值被设置时,如果与旧值不同,将触发 OnCurrentSelectedChanged 事件。
/// </summary>
public int currentSelected
{
get => _currentSelected;
set
{
var maxIndex = Inventory != null && Inventory.Capacity > 0 ? Inventory.Capacity - 1 : 0;
var clampedValue = Mathf.Clamp(value, 0, maxIndex);
if (_currentSelected != clampedValue)
{
_currentSelected = clampedValue;
OnCurrentSelectedChanged?.Invoke(_currentSelected); // 触发事件
}
}
}
public Inventory Inventory { get; private set; }
/// <summary>
/// 当当前选中的槽位索引改变时触发的事件。
/// 参数:新的选中索引。
/// </summary>
public event Action<int> OnCurrentSelectedChanged;
public override void Init(EntityDef entityDef)
{
base.Init(entityDef);
// 1. 初始化背包:
// 在这里创建并初始化 Inventory 实例。
// 'this' 指的是当前 Character 实例,作为 Inventory 的拥有者。
// 容量可以根据 entityDef 或一个默认值来设定。这里使用默认值 20。
Inventory = new Inventory(this, 3);
Inventory = new Inventory(this, 3);
// (可选) 可以在此订阅背包事件,以便 Character 对背包变化做出响应,
// 例如更新角色状态、播放音效或更新UI
// Inventory.OnItemAdded += HandleItemAddedToInventory;
// Inventory.OnItemRemoved += HandleItemRemovedFromInventory;
// Inventory.OnInventoryChanged += HandleInventoryChanged;
// 初始化 currentSelected。
// 使用属性来设置,确保触发事件和范围检查
// 如果Inventory.Capacity为0则currentSelected会被钳制到0。
// 如果Inventory.Capacity为3currentSelected=0是有效值。
currentSelected = 0;
}
/// <summary>
/// 尝试将指定物品添加到角色的背包中。
/// 尝试将指定物品添加到角色的背包中。
/// </summary>
/// <param name="itemResource">要尝试添加的物品资源。</param>
/// <param name="quantity">要尝试添加的数量。</param>
/// <returns>未成功添加到背包的物品数量。
/// 如果返回0表示所有物品都成功添加
/// 如果返回quantity表示未能添加任何物品
/// 如果返回一个介于0和quantity之间的值表示部分物品被添加。</returns>
public int TryPickupItem(Item.ItemResource itemResource, int quantity)
/// <returns>
/// 未成功添加到背包的物品数量。
/// 如果返回0表示所有物品都成功添加
/// 如果返回quantity表示未能添加任何物品
/// 如果返回一个介于0和quantity之间的值表示部分物品被添加。
/// </returns>
public int TryPickupItem(ItemResource itemResource, int quantity)
{
if (Inventory == null)
{
Debug.LogError($"Character '{this.name}' inventory is not initialized. Cannot pickup item.");
Debug.LogError($"Character '{name}' inventory is not initialized. Cannot pickup item.");
return quantity; // 如果背包未初始化,则视为未能添加任何物品
}
// 直接调用 Inventory 实例的 AddItem 方法。
// AddItem 方法已经包含了严谨的逻辑来处理物品堆叠、新槽位分配、容量检查以及无效输入的警告。
int remainingQuantity = Inventory.AddItem(itemResource, quantity);
var remainingQuantity = Inventory.AddItem(itemResource, quantity);
return remainingQuantity;
}
// (可选) 示例事件处理方法,用于展示如何响应背包事件
// private void HandleItemAddedToInventory(Item.ItemResource item, int count)
// {
// Debug.Log($"{DefName} picked up {count} x {item.Name}. Current total: {Inventory.GetItemCount(item)}");
// // 例如更新UI、播放音效、完成任务等
// }
// private void HandleItemRemovedFromInventory(Item.ItemResource item, int count)
// {
// Debug.Log($"{DefName} removed {count} x {item.Name}. Current total: {Inventory.GetItemCount(item)}");
// // 例如更新UI、播放音效、更新制作状态等
// }
// private void HandleInventoryChanged()
// {
// Debug.Log($"{DefName}'s inventory changed. Occupied slots: {Inventory.OccupiedSlotsCount}/{Inventory.Capacity}");
// // 例如通知背包UI刷新显示
// }
// ------------- 新增 / 修改部分 END -------------
public override void TryAttack()
{
if (IsAttacking)
return;
if (!Managers.DefineManager.Instance.defines.TryGetValue(nameof(BulletDef), out var def))
if (!DefineManager.Instance.defines.TryGetValue(nameof(BulletDef), out var def))
return;
var buttonDef = def.Values.First();
Vector3 dir = Utils.MousePosition.GetWorldPosition();
Managers.EntityManage.Instance.GenerateBulletEntity(Program.Instance.focuseDimensionId,(BulletDef)buttonDef, Position,
// 修正First() 可能会在一个空的 Values 集合上抛出异常。
// 更好的做法是使用 TryGetValue 或 FirstOrDefault 并检查结果。
// 这里假设至少有一个 BulletDef 存在,如果不是,需要更复杂的错误处理。
var bulletDefEntry = def.Values.FirstOrDefault();
if (bulletDefEntry == null)
{
Debug.LogError("No BulletDef found in DefineManager. Cannot attack.");
return;
}
var bulletDef = (BulletDef)bulletDefEntry;
Vector3 dir = MousePosition.GetWorldPosition();
EntityManage.Instance.GenerateBulletEntity(Program.Instance.FocusedDimensionId, bulletDef, Position,
dir - Position, this);
}
public override WeaponResource GetCurrentWeapon()
{
var currentSelectItem = Inventory.GetSlot(currentSelected);
return (WeaponResource)currentSelectItem.Item;
}
}
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using AI;
using Base;
using Data;
using Item;
using Prefab;
using UnityEngine;
@ -86,19 +87,19 @@ namespace Entity
IsChase = true;
currentJob = null;
// 逻辑修改只有当存在一个不同的焦点实体时才将其PlayerControlled设为false
if (Program.Instance.focusedEntity && Program.Instance.focusedEntity != this)
if (Program.Instance.FocusedEntity && Program.Instance.FocusedEntity != this)
{
Program.Instance.focusedEntity.PlayerControlled = false;
Program.Instance.FocusedEntity.PlayerControlled = false;
}
Program.Instance.focusedEntity = this;
Program.Instance.SetFocusedEntity(this);
}
// 逻辑修改:确保只有当自身是焦点实体时才取消焦点,避免不必要的逻辑执行
else if (Program.Instance.focusedEntity == this)
else if (Program.Instance.FocusedEntity == this)
{
Program.Instance.focusedEntity = null;
Program.Instance.SetFocusedEntity(null);
}
}
get => Program.Instance.focusedEntity == this;
get => Program.Instance.FocusedEntity == this;
}
@ -115,18 +116,27 @@ namespace Entity
public bool IsDead => attributes.health <= 0;
public bool IsShowingHealthBarUI=>_hitBarUIShowTimer > 0;
public bool IsAttacking => _attackCoroutine != null;
/// <summary>
/// 当实体受到伤害时触发的事件。
/// 可以订阅此事件来响应实体的生命值变化例如更新UI或播放受击特效。
/// </summary>
public event Action<EntityHitEventArgs> OnEntityHit;
/// <summary>
/// 当实体死亡时触发的事件。
/// 只在实体首次进入死亡状态时触发一次。
/// </summary>
public event Action<Entity> OnEntityDied;
private bool _warning = false;
/// <summary>
/// 存储不同朝向下的动画节点集合。
/// </summary>
public Dictionary<EntityState, Dictionary<Orientation, List<ITick>>> bodyAnimationNode = new();
private List<ITick> _currentAnimatorCache=new ();
public Dictionary<EntityState, Dictionary<Orientation, ITick[]>> bodyAnimationNode = new();
private ITick[] _currentAnimatorCache;
/// <summary>
/// 存储不同朝向下的身体节点对象。
/// </summary>
private Dictionary<EntityState, Dictionary<Orientation,GameObject>> bodyNodes = new();
protected Dictionary<EntityState, Dictionary<Orientation,GameObject>> bodyNodes = new();
/// <summary>
/// 当前实体的朝向。
@ -186,7 +196,7 @@ namespace Entity
foreach (var state in states)
{
bodyNodes.TryAdd(state, new Dictionary<Orientation, GameObject>());
bodyAnimationNode.TryAdd(state, new Dictionary<Orientation, List<ITick>>());
bodyAnimationNode.TryAdd(state, new Dictionary<Orientation, ITick[]>());
}
// 主初始化逻辑
@ -229,7 +239,7 @@ namespace Entity
{
animatorsForOrientation.AddRange(animators);
}
stateAnimNodes[orientation] = animatorsForOrientation;
stateAnimNodes[orientation] = animatorsForOrientation.ToArray();
}
}
@ -472,7 +482,7 @@ namespace Entity
}
else
{
_currentAnimatorCache = new List<ITick>(); // 如果没有找到动画,则使用空列表
_currentAnimatorCache = new ITick[]{}; // 如果没有找到动画,则使用空列表
}
}
@ -487,19 +497,38 @@ namespace Entity
SetBodyTexture(EntityState.Walking,_currentOrientation);
_walkingTimer = 2;
}
/// <summary>
/// 处理实体受到攻击的逻辑。
/// </summary>
/// <param name="from">攻击来源实体。</param>
public virtual void OnHit(Entity from)
{
// MODIFIED: 收到攻击时触发回调
if (IsDead) // 如果已经死亡,则不再处理伤害 nor 触发事件
{
return;
}
var hit = from.attributes.attack - attributes.defense;
if (hit < 0)
hit = from.attributes.attack / 100;
// 确保伤害不为负最小为0
hit = Mathf.Max(0, hit);
attributes.health -= hit;
var wasFatal = IsDead; // 检查这次攻击是否导致实体死亡
// 触发 OnEntityHit 事件
OnEntityHit?.Invoke(new EntityHitEventArgs(
this, from, hit, attributes.health, entityDef.attributes.health, wasFatal));
currentJob?.StopJob();
ShowHealthBar();
if (wasFatal)
{
// 如果是首次死亡,则触发 OnEntityDied 事件
// MODIFIED: 停止所有活动,包括当前工作
currentJob = null; // 清除当前工作
OnEntityDied?.Invoke(this);
}
ShowHealthBar(); // 无论是否死亡都更新血条UI
}
public void ShowHealthBar()
@ -523,7 +552,17 @@ namespace Entity
/// </summary>
public virtual void Kill()
{
attributes.health = 0;
if (IsDead)
{
return;
}
attributes.health = 0; // 直接设置生命值为0
// MODIFIED: 停止所有活动,包括当前工作
currentJob?.StopJob();
currentJob = null; // 清除当前工作
// 触发 OnEntityDied 事件
OnEntityDied?.Invoke(this);
ShowHealthBar();
}
/// <summary>
@ -684,5 +723,10 @@ namespace Entity
attackCount--;
}
}
public virtual WeaponResource GetCurrentWeapon()
{
return null;
}
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace Entity
{
public class EntityHitEventArgs : EventArgs
{
public Entity AttackedEntity { get; } // 被攻击的实体
public Entity AttackerEntity { get; } // 攻击者
public int DamageDealt { get; } // 这次攻击造成的实际伤害
public float CurrentHealth { get; } // 攻击后的当前生命值
public float MaxHealth { get; } // 最大生命值
public bool WasFatal { get; } // 这次攻击是否致命(导致死亡)
public EntityHitEventArgs(Entity attacked, Entity attacker, int damage, float currentHealth, float maxHealth, bool wasFatal)
{
AttackedEntity = attacked;
AttackerEntity = attacker;
DamageDealt = damage;
CurrentHealth = currentHealth;
MaxHealth = maxHealth;
WasFatal = wasFatal;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5d160d50370b46b299421e6c05a83beb
timeCreated: 1756199871

View File

@ -142,6 +142,25 @@ namespace Entity
{
return _slots.AsReadOnly();
}
public InventorySlot GetSlot(Item.ItemResource itemResource)
{
if (itemResource == null) return null;
foreach (var slot in _slots)
{
if (slot.Item != null && slot.Item.DefName == itemResource.DefName)
{
return slot;
}
}
return null;
}
public InventorySlot GetSlot(int i)
{
i = Mathf.Clamp(i, 0, Capacity - 1);
return _slots[i];
}
public int OccupiedSlotsCount => _slots.Count;
public int AvailableSlotsCount => Capacity - _slots.Count;

View File

@ -116,7 +116,7 @@ namespace Entity
private void BecomeDefault()
{
entity.Kill();
EntityManage.Instance.GenerateDefaultEntity(Program.Instance.focuseDimensionId,entity.Position);
EntityManage.Instance.GenerateDefaultEntity(Program.Instance.FocusedDimensionId,entity.Position);
}
private void StartControl()

View File

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Base;
using Data;
using Item;
using Prefab;
using UnityEngine;
namespace Entity
@ -11,6 +15,48 @@ namespace Entity
protected override void AutoBehave()
{
}
public override void SetTarget(Vector3 pos)
{
}
public void Init(ItemDef itemDef)
{
itemResource = Managers.ItemResourceManager.Instance.GetItem(itemDef.defName);
// 预缓存枚举值(避免每次循环重复调用 Enum.GetValues
var states = Enum.GetValues(typeof(EntityState)).Cast<EntityState>().ToArray();
// 预初始化字典结构(减少内层循环的字典检查)
foreach (var state in states)
{
bodyNodes.TryAdd(state, new Dictionary<Orientation, GameObject>());
bodyAnimationNode.TryAdd(state, new Dictionary<Orientation, ITick[]>());
}
var texture = itemResource.Icon;
if (texture.Count == 1)
{
var imageObj = Instantiate(imagePrefab.gameObject, body.transform);
imageObj.transform.localPosition = Vector3.zero;
bodyNodes[EntityState.Idle][Orientation.Down] = imageObj;
var image = imageObj.GetComponent<ImagePrefab>();
image.SetSprite(texture[0]);
}
else if (texture.Count > 1)
{
var animatorObj = Instantiate(animatorPrefab.gameObject, body.transform);
animatorObj.transform.localPosition = Vector3.zero;
bodyNodes[EntityState.Idle][Orientation.Down] = animatorObj;
var animator = animatorObj.GetComponent<SpriteAnimator>();
animator.SetSprites(texture.ToArray());
ITick[] ticks = { animator };
bodyAnimationNode[EntityState.Idle][Orientation.Down] = ticks;
}
SetBodyTexture(EntityState.Idle, Orientation.Down);
}
private void OnTriggerEnter2D(Collider2D other)
{
var entity = other.GetComponent<Character>();