코드 리팩토링
재사용성 및 확장성을 고려하여 코드 전반을 리팩토링함
This commit is contained in:
20
Assets/Scripts/Player/BehaviorActionData.cs
Normal file
20
Assets/Scripts/Player/BehaviorActionData.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Bridge class that wraps ItemBehavior for use with PlayerActionHandler.
|
||||
/// This allows the new behavior system to work with the existing action handler.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Actions/Behavior Action")]
|
||||
public class BehaviorActionData : PlayerActionData
|
||||
{
|
||||
[HideInInspector]
|
||||
public ItemBehavior behavior;
|
||||
|
||||
public override void ExecuteEffect(GameObject performer, GameObject target)
|
||||
{
|
||||
if (behavior != null)
|
||||
{
|
||||
behavior.Use(performer, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/BehaviorActionData.cs.meta
Normal file
2
Assets/Scripts/Player/BehaviorActionData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a373ecb07ad66848923d4a455b6d236
|
||||
50
Assets/Scripts/Player/ConsumableBehavior.cs
Normal file
50
Assets/Scripts/Player/ConsumableBehavior.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Consumable behavior for healing items, food, etc.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Items/Behaviors/Consumable Behavior")]
|
||||
public class ConsumableBehavior : ItemBehavior
|
||||
{
|
||||
[Header("Consumable Settings")]
|
||||
[SerializeField] private float healAmount = 20f;
|
||||
[SerializeField] private float staminaRestore = 0f;
|
||||
|
||||
public override bool IsConsumable => true;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Consume";
|
||||
if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Consume";
|
||||
canRepeat = false;
|
||||
}
|
||||
|
||||
public override bool CanUse(GameObject user, GameObject target)
|
||||
{
|
||||
// Can use if user has a health component that isn't at full health
|
||||
var health = user.GetComponent<HealthComponent>();
|
||||
if (health == null) return false;
|
||||
|
||||
// Can use if not at full health (healing) or if it restores stamina
|
||||
return !health.IsAtFullHealth() || staminaRestore > 0;
|
||||
}
|
||||
|
||||
public override void Use(GameObject user, GameObject target)
|
||||
{
|
||||
var health = user.GetComponent<HealthComponent>();
|
||||
if (health != null && healAmount > 0)
|
||||
{
|
||||
health.Heal(healAmount);
|
||||
}
|
||||
|
||||
// Stamina restoration would go here when stamina system is implemented
|
||||
}
|
||||
|
||||
public override string GetBlockedReason(GameObject user, GameObject target)
|
||||
{
|
||||
var health = user.GetComponent<HealthComponent>();
|
||||
if (health == null) return "Cannot use this item";
|
||||
if (health.IsAtFullHealth() && staminaRestore <= 0) return "Already at full health";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/ConsumableBehavior.cs.meta
Normal file
2
Assets/Scripts/Player/ConsumableBehavior.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9c4b3ac4b03db34fa97481232baadfe
|
||||
230
Assets/Scripts/Player/EquipmentSlot.cs
Normal file
230
Assets/Scripts/Player/EquipmentSlot.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Types of equipment slots available.
|
||||
/// </summary>
|
||||
public enum EquipmentSlotType
|
||||
{
|
||||
MainHand,
|
||||
OffHand,
|
||||
Head,
|
||||
Body,
|
||||
Back,
|
||||
Accessory
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single equipment slot that can hold an equipped item.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class EquipmentSlot
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of this equipment slot.
|
||||
/// </summary>
|
||||
public EquipmentSlotType SlotType;
|
||||
|
||||
/// <summary>
|
||||
/// Transform where equipment is attached.
|
||||
/// </summary>
|
||||
public Transform AttachPoint;
|
||||
|
||||
/// <summary>
|
||||
/// Currently spawned equipment instance.
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
public GameObject CurrentEquipment;
|
||||
|
||||
/// <summary>
|
||||
/// Currently equipped item data.
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
public ItemData EquippedItem;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when equipment changes.
|
||||
/// </summary>
|
||||
public event Action<EquipmentSlot, ItemData> OnEquipmentChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Equip an item to this slot.
|
||||
/// </summary>
|
||||
/// <param name="item">Item to equip (or null to unequip)</param>
|
||||
public void Equip(ItemData item)
|
||||
{
|
||||
// Remove current equipment
|
||||
Unequip();
|
||||
|
||||
if (item == null) return;
|
||||
|
||||
// Check if item can be equipped
|
||||
if (!item.CanBeEquipped) return;
|
||||
|
||||
EquippedItem = item;
|
||||
|
||||
// Spawn equipment visual
|
||||
var prefab = item.GetEquipmentPrefab();
|
||||
if (prefab != null && AttachPoint != null)
|
||||
{
|
||||
CurrentEquipment = UnityEngine.Object.Instantiate(prefab, AttachPoint);
|
||||
CurrentEquipment.transform.localPosition = item.GetPositionOffset();
|
||||
CurrentEquipment.transform.localRotation = Quaternion.Euler(item.GetRotationOffset());
|
||||
}
|
||||
|
||||
OnEquipmentChanged?.Invoke(this, item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equip using IEquippableItem interface (more generic).
|
||||
/// </summary>
|
||||
/// <param name="equippable">Equippable item</param>
|
||||
/// <param name="user">The user equipping the item</param>
|
||||
public void Equip(IEquippableItem equippable, GameObject user)
|
||||
{
|
||||
Unequip();
|
||||
|
||||
if (equippable == null) return;
|
||||
|
||||
var prefab = equippable.GetEquipmentPrefab();
|
||||
if (prefab == null) return;
|
||||
|
||||
// Find or use the configured attach point
|
||||
Transform attachTo = AttachPoint;
|
||||
if (attachTo == null && user != null)
|
||||
{
|
||||
attachTo = equippable.FindAttachmentPoint(user);
|
||||
}
|
||||
|
||||
if (attachTo != null)
|
||||
{
|
||||
CurrentEquipment = UnityEngine.Object.Instantiate(prefab, attachTo);
|
||||
CurrentEquipment.transform.localPosition = equippable.GetPositionOffset();
|
||||
CurrentEquipment.transform.localRotation = Quaternion.Euler(equippable.GetRotationOffset());
|
||||
}
|
||||
|
||||
OnEquipmentChanged?.Invoke(this, EquippedItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unequip the current item.
|
||||
/// </summary>
|
||||
public void Unequip()
|
||||
{
|
||||
if (CurrentEquipment != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(CurrentEquipment);
|
||||
CurrentEquipment = null;
|
||||
}
|
||||
|
||||
var previousItem = EquippedItem;
|
||||
EquippedItem = null;
|
||||
|
||||
if (previousItem != null)
|
||||
{
|
||||
OnEquipmentChanged?.Invoke(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if this slot has equipment.
|
||||
/// </summary>
|
||||
public bool HasEquipment => CurrentEquipment != null || EquippedItem != null;
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific item can be equipped in this slot.
|
||||
/// </summary>
|
||||
public bool CanEquip(ItemData item)
|
||||
{
|
||||
if (item == null) return true; // Can always "unequip"
|
||||
return item.CanBeEquipped;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manages multiple equipment slots for a character.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class EquipmentManager
|
||||
{
|
||||
[SerializeField]
|
||||
private EquipmentSlot[] _slots;
|
||||
|
||||
/// <summary>
|
||||
/// All equipment slots.
|
||||
/// </summary>
|
||||
public EquipmentSlot[] Slots => _slots;
|
||||
|
||||
/// <summary>
|
||||
/// Get a slot by type.
|
||||
/// </summary>
|
||||
public EquipmentSlot GetSlot(EquipmentSlotType type)
|
||||
{
|
||||
if (_slots == null) return null;
|
||||
|
||||
foreach (var slot in _slots)
|
||||
{
|
||||
if (slot.SlotType == type)
|
||||
return slot;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equip an item to the appropriate slot.
|
||||
/// </summary>
|
||||
public bool TryEquip(ItemData item, EquipmentSlotType preferredSlot = EquipmentSlotType.MainHand)
|
||||
{
|
||||
var slot = GetSlot(preferredSlot);
|
||||
if (slot != null && slot.CanEquip(item))
|
||||
{
|
||||
slot.Equip(item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unequip all slots.
|
||||
/// </summary>
|
||||
public void UnequipAll()
|
||||
{
|
||||
if (_slots == null) return;
|
||||
|
||||
foreach (var slot in _slots)
|
||||
{
|
||||
slot.Unequip();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize slots with attach points found on the character.
|
||||
/// </summary>
|
||||
public void Initialize(GameObject character, params (EquipmentSlotType type, string attachPointName)[] slotConfigs)
|
||||
{
|
||||
_slots = new EquipmentSlot[slotConfigs.Length];
|
||||
|
||||
for (int i = 0; i < slotConfigs.Length; i++)
|
||||
{
|
||||
var config = slotConfigs[i];
|
||||
Transform attachPoint = null;
|
||||
|
||||
// Find attach point
|
||||
var transforms = character.GetComponentsInChildren<Transform>();
|
||||
foreach (var t in transforms)
|
||||
{
|
||||
if (t.name == config.attachPointName)
|
||||
{
|
||||
attachPoint = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_slots[i] = new EquipmentSlot
|
||||
{
|
||||
SlotType = config.type,
|
||||
AttachPoint = attachPoint
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/EquipmentSlot.cs.meta
Normal file
2
Assets/Scripts/Player/EquipmentSlot.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2bee3e86fbe00446b94cf38066b8a81
|
||||
@@ -1,13 +1,171 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Requirement for performing an action (item cost, etc.).
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct ActionRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Item ID required (use -1 if no item required).
|
||||
/// </summary>
|
||||
public int ItemID;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of the item required.
|
||||
/// </summary>
|
||||
public int Amount;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the item is consumed when the action is performed.
|
||||
/// </summary>
|
||||
public bool ConsumeOnUse;
|
||||
|
||||
public ActionRequirement(int itemID, int amount, bool consumeOnUse = true)
|
||||
{
|
||||
ItemID = itemID;
|
||||
Amount = amount;
|
||||
ConsumeOnUse = consumeOnUse;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// No requirement.
|
||||
/// </summary>
|
||||
public static ActionRequirement None => new ActionRequirement(-1, 0, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes an action that can be performed.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class ActionDescriptor
|
||||
{
|
||||
public float duration = 0.5f;
|
||||
public string animTrigger = "Interact";
|
||||
// 필요하다면 여기에 사운드 이펙트나 파티클 정보를 추가할 수 있습니다.
|
||||
/// <summary>
|
||||
/// Display name of the action.
|
||||
/// </summary>
|
||||
public string ActionName = "Action";
|
||||
|
||||
/// <summary>
|
||||
/// Total duration of the action in seconds.
|
||||
/// </summary>
|
||||
public float Duration = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Animation trigger name.
|
||||
/// </summary>
|
||||
public string AnimTrigger = "Interact";
|
||||
|
||||
/// <summary>
|
||||
/// Animation playback speed multiplier.
|
||||
/// </summary>
|
||||
public float AnimSpeed = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Time within the animation when the effect occurs (for syncing hit with animation).
|
||||
/// </summary>
|
||||
public float ImpactDelay = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Sound effect to play.
|
||||
/// </summary>
|
||||
public AudioClip SoundEffect;
|
||||
|
||||
/// <summary>
|
||||
/// Particle effect prefab to spawn.
|
||||
/// </summary>
|
||||
public GameObject ParticleEffect;
|
||||
|
||||
/// <summary>
|
||||
/// Stamina cost to perform this action.
|
||||
/// </summary>
|
||||
public float StaminaCost = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Item requirements for this action.
|
||||
/// </summary>
|
||||
public ActionRequirement[] ItemRequirements;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this action can be repeated by holding the button.
|
||||
/// </summary>
|
||||
public bool CanRepeat = false;
|
||||
|
||||
/// <summary>
|
||||
/// Cooldown time before this action can be performed again.
|
||||
/// </summary>
|
||||
public float Cooldown = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Create a simple action descriptor.
|
||||
/// </summary>
|
||||
public static ActionDescriptor Simple(string name, float duration, string animTrigger = "Interact")
|
||||
{
|
||||
return new ActionDescriptor
|
||||
{
|
||||
ActionName = name,
|
||||
Duration = duration,
|
||||
AnimTrigger = animTrigger
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an action descriptor for repeatable actions (like mining).
|
||||
/// </summary>
|
||||
public static ActionDescriptor Repeatable(string name, float duration, string animTrigger,
|
||||
float impactDelay, float animSpeed = 1f)
|
||||
{
|
||||
return new ActionDescriptor
|
||||
{
|
||||
ActionName = name,
|
||||
Duration = duration,
|
||||
AnimTrigger = animTrigger,
|
||||
AnimSpeed = animSpeed,
|
||||
ImpactDelay = impactDelay,
|
||||
CanRepeat = true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 명세를 제공하는 인터페이스
|
||||
/// <summary>
|
||||
/// Interface for objects that can provide action descriptors.
|
||||
/// Implement this to define what actions can be performed on or with an object.
|
||||
/// </summary>
|
||||
public interface IActionProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the primary action descriptor for this provider.
|
||||
/// </summary>
|
||||
ActionDescriptor GetActionDescriptor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all available actions from this provider.
|
||||
/// Default implementation returns only the primary action.
|
||||
/// </summary>
|
||||
IEnumerable<ActionDescriptor> GetAvailableActions()
|
||||
{
|
||||
yield return GetActionDescriptor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific action can be performed.
|
||||
/// </summary>
|
||||
/// <param name="performer">The GameObject attempting the action</param>
|
||||
/// <param name="action">The action to check</param>
|
||||
/// <returns>True if the action can be performed</returns>
|
||||
bool CanPerformAction(GameObject performer, ActionDescriptor action)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the reason why an action cannot be performed.
|
||||
/// </summary>
|
||||
/// <param name="performer">The GameObject attempting the action</param>
|
||||
/// <param name="action">The action to check</param>
|
||||
/// <returns>Reason string, or null if action can be performed</returns>
|
||||
string GetActionBlockedReason(GameObject performer, ActionDescriptor action)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,12 @@ public class MiningActionData : PlayerActionData
|
||||
|
||||
public override void ExecuteEffect(GameObject performer, GameObject target)
|
||||
{
|
||||
if(target == null) return;
|
||||
if (target == null) return;
|
||||
|
||||
if (target.TryGetComponent<MineableBlock>(out var block))
|
||||
// Use IDamageable interface for all damageable objects
|
||||
if (target.TryGetComponent<IDamageable>(out var damageable))
|
||||
{
|
||||
// 서버 RPC 호출은 블록 내부의 로직을 그대로 사용합니다.
|
||||
block.TakeDamageRpc(damage);
|
||||
block.PlayHitEffectClientRpc();
|
||||
damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, performer));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
44
Assets/Scripts/Player/MiningBehavior.cs
Normal file
44
Assets/Scripts/Player/MiningBehavior.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Mining behavior for pickaxes and similar tools.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Items/Behaviors/Mining Behavior")]
|
||||
public class MiningBehavior : ItemBehavior
|
||||
{
|
||||
[Header("Mining Settings")]
|
||||
[SerializeField] private int damage = 50;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Set default mining values
|
||||
if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Mine";
|
||||
if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Attack";
|
||||
canRepeat = true;
|
||||
}
|
||||
|
||||
public override bool CanUse(GameObject user, GameObject target)
|
||||
{
|
||||
// Can always swing, but only deals damage if hitting a mineable block
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Use(GameObject user, GameObject target)
|
||||
{
|
||||
if (target == null) return;
|
||||
|
||||
// Use IDamageable interface for all damageable objects
|
||||
if (target.TryGetComponent<IDamageable>(out var damageable))
|
||||
{
|
||||
damageable.TakeDamage(new DamageInfo(damage, DamageType.Mining, user));
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetBlockedReason(GameObject user, GameObject target)
|
||||
{
|
||||
if (target == null) return "No target";
|
||||
if (!target.TryGetComponent<IDamageable>(out _))
|
||||
return "Cannot mine this object";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/MiningBehavior.cs.meta
Normal file
2
Assets/Scripts/Player/MiningBehavior.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c1deeb9de56edff4ca77ddabf9db691a
|
||||
41
Assets/Scripts/Player/PlaceableBehavior.cs
Normal file
41
Assets/Scripts/Player/PlaceableBehavior.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Placeable behavior for building/placing items.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(menuName = "Items/Behaviors/Placeable Behavior")]
|
||||
public class PlaceableBehavior : ItemBehavior
|
||||
{
|
||||
[Header("Placement Settings")]
|
||||
[SerializeField] private GameObject placeablePrefab;
|
||||
[SerializeField] private bool requiresGround = true;
|
||||
[SerializeField] private float placementRange = 5f;
|
||||
|
||||
public override bool IsConsumable => true;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (string.IsNullOrEmpty(behaviorName)) behaviorName = "Place";
|
||||
if (string.IsNullOrEmpty(animTrigger)) animTrigger = "Place";
|
||||
canRepeat = false;
|
||||
}
|
||||
|
||||
public override bool CanUse(GameObject user, GameObject target)
|
||||
{
|
||||
// Would integrate with BuildManager for placement validation
|
||||
return placeablePrefab != null;
|
||||
}
|
||||
|
||||
public override void Use(GameObject user, GameObject target)
|
||||
{
|
||||
// Actual placement would be handled by BuildManager
|
||||
// This is a placeholder for the behavior pattern
|
||||
Debug.Log($"[PlaceableBehavior] Would place {placeablePrefab?.name}");
|
||||
}
|
||||
|
||||
public override string GetBlockedReason(GameObject user, GameObject target)
|
||||
{
|
||||
if (placeablePrefab == null) return "Invalid placement item";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player/PlaceableBehavior.cs.meta
Normal file
2
Assets/Scripts/Player/PlaceableBehavior.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b4962abe690c6ef47b7ea654ce747200
|
||||
@@ -33,11 +33,11 @@ public class PlayerActionHandler : NetworkBehaviour
|
||||
private IEnumerator InteractionRoutine(ActionDescriptor desc, IInteractable target)
|
||||
{
|
||||
_isBusy = true;
|
||||
if (desc != null) _animator.SetTrigger(desc.animTrigger);
|
||||
if (desc != null) _animator.SetTrigger(desc.AnimTrigger);
|
||||
|
||||
target.Interact(gameObject); // 로직 실행
|
||||
|
||||
yield return new WaitForSeconds(desc?.duration ?? 0.1f);
|
||||
yield return new WaitForSeconds(desc?.Duration ?? 0.1f);
|
||||
_isBusy = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,63 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Handles equipment visuals for the player.
|
||||
/// Uses the new EquipmentSlot system while maintaining backwards compatibility.
|
||||
/// </summary>
|
||||
public class PlayerEquipmentHandler : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private Transform toolAnchor; // 캐릭터 손의 소켓 위치
|
||||
[Header("Equipment Settings")]
|
||||
[SerializeField] private Transform mainHandAnchor;
|
||||
[SerializeField] private Transform offHandAnchor;
|
||||
|
||||
[Header("Slot Configuration")]
|
||||
[SerializeField] private EquipmentSlot mainHandSlot;
|
||||
|
||||
private PlayerInventory _inventory;
|
||||
private GameObject _currentToolInstance; // 현재 생성된 도구 모델
|
||||
private GameObject _currentToolInstance;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
_inventory = GetComponent<PlayerInventory>();
|
||||
|
||||
// Initialize main hand slot if not configured in inspector
|
||||
if (mainHandSlot == null)
|
||||
{
|
||||
mainHandSlot = new EquipmentSlot
|
||||
{
|
||||
SlotType = EquipmentSlotType.MainHand,
|
||||
AttachPoint = mainHandAnchor
|
||||
};
|
||||
}
|
||||
else if (mainHandSlot.AttachPoint == null)
|
||||
{
|
||||
mainHandSlot.AttachPoint = mainHandAnchor;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
// 인벤토리의 슬롯 변경 이벤트 구독
|
||||
// OnSlotChanged는 (이전 값, 새 값) 두 개의 인자를 전달합니다.
|
||||
_inventory.OnSlotChanged += HandleSlotChanged;
|
||||
// Subscribe to inventory slot changes
|
||||
if (_inventory != null)
|
||||
{
|
||||
_inventory.OnSlotChanged += HandleSlotChanged;
|
||||
|
||||
// 게임 시작 시 처음에 들고 있는 아이템 모델 생성
|
||||
UpdateEquippedModel(_inventory.SelectedSlotIndex);
|
||||
// Initialize with current slot
|
||||
UpdateEquippedModel(_inventory.SelectedSlotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
// Unsubscribe to prevent memory leaks
|
||||
if (_inventory != null)
|
||||
{
|
||||
_inventory.OnSlotChanged -= HandleSlotChanged;
|
||||
}
|
||||
|
||||
// Clean up equipment
|
||||
mainHandSlot?.Unequip();
|
||||
}
|
||||
|
||||
private void HandleSlotChanged(int previousValue, int newValue)
|
||||
@@ -29,30 +67,58 @@ public class PlayerEquipmentHandler : NetworkBehaviour
|
||||
|
||||
private void UpdateEquippedModel(int slotIndex)
|
||||
{
|
||||
// 1. 기존 도구가 있다면 파괴
|
||||
if (_currentToolInstance != null)
|
||||
// Get item data for the selected slot
|
||||
ItemData data = _inventory?.GetItemDataInSlot(slotIndex);
|
||||
|
||||
// Use new equipment slot system
|
||||
if (data != null && data.CanBeEquipped)
|
||||
{
|
||||
Destroy(_currentToolInstance);
|
||||
// Use IEquippableItem interface
|
||||
mainHandSlot.Equip(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
mainHandSlot.Unequip();
|
||||
}
|
||||
|
||||
// 2. 현재 선택된 슬롯의 데이터 확인
|
||||
ItemData data = _inventory.GetItemDataInSlot(slotIndex);
|
||||
|
||||
// 3. 도구인 경우에만 모델 생성
|
||||
if (data != null && data.isTool && data.toolPrefab != null)
|
||||
{
|
||||
_currentToolInstance = Instantiate(data.toolPrefab, toolAnchor);
|
||||
|
||||
// ItemData에 설정된 오프셋 적용
|
||||
_currentToolInstance.transform.localPosition = data.equipPositionOffset;
|
||||
_currentToolInstance.transform.localRotation = Quaternion.Euler(data.equipRotationOffset);
|
||||
}
|
||||
// Update legacy reference for any code that might check it
|
||||
_currentToolInstance = mainHandSlot.CurrentEquipment;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
/// <summary>
|
||||
/// Get the currently equipped item data.
|
||||
/// </summary>
|
||||
public ItemData GetEquippedItem()
|
||||
{
|
||||
// 이벤트 구독 해제 (메모리 누수 방지)
|
||||
if (_inventory != null)
|
||||
_inventory.OnSlotChanged -= HandleSlotChanged;
|
||||
return mainHandSlot?.EquippedItem;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the currently equipped tool instance.
|
||||
/// </summary>
|
||||
public GameObject GetCurrentToolInstance()
|
||||
{
|
||||
return mainHandSlot?.CurrentEquipment ?? _currentToolInstance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if player has equipment in main hand.
|
||||
/// </summary>
|
||||
public bool HasMainHandEquipment => mainHandSlot?.HasEquipment ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Force refresh the equipped model.
|
||||
/// </summary>
|
||||
public void RefreshEquipment()
|
||||
{
|
||||
if (_inventory != null)
|
||||
{
|
||||
UpdateEquippedModel(_inventory.SelectedSlotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the main hand equipment slot for advanced usage.
|
||||
/// </summary>
|
||||
public EquipmentSlot MainHandSlot => mainHandSlot;
|
||||
}
|
||||
|
||||
@@ -204,35 +204,50 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
if (_isGrounded) _velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||
}
|
||||
|
||||
// 1. 액션 (좌클릭) - 대상이 없어도 나감
|
||||
// PlayerNetworkController.cs 중 일부
|
||||
// 1. Action (Left Click) - executes even without target
|
||||
private void OnActionInput()
|
||||
{
|
||||
if (!IsOwner || _actionHandler.IsBusy) return;
|
||||
|
||||
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||
if (selectedItem == null) return;
|
||||
|
||||
// 로그 1: 아이템 확인
|
||||
if (selectedItem == null) { Debug.Log("선택된 아이템이 없음"); return; }
|
||||
|
||||
// 로그 2: 도구 여부 및 액션 데이터 확인
|
||||
Debug.Log($"현재 아이템: {selectedItem.itemName}, 도구여부: {selectedItem.isTool}, 액션데이터: {selectedItem.toolAction != null}");
|
||||
|
||||
if (selectedItem.isTool && selectedItem.toolAction != null)
|
||||
// Check if item has behavior (new system)
|
||||
if (selectedItem.behavior != null)
|
||||
{
|
||||
if (_lastHighlightedBlock != null)
|
||||
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
|
||||
|
||||
// Use the new behavior system
|
||||
if (selectedItem.CanUse(gameObject, target))
|
||||
{
|
||||
Debug.Log($"채광 시작: {_lastHighlightedBlock.name}");
|
||||
_actionHandler.PerformAction(selectedItem.toolAction, _lastHighlightedBlock.gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("조준된 블록이 없음 (하이라이트 확인 필요)");
|
||||
_actionHandler.PerformAction(selectedItem.toolAction, null);
|
||||
// Get action descriptor and perform action
|
||||
var actionDesc = selectedItem.GetUseAction();
|
||||
if (actionDesc != null)
|
||||
{
|
||||
_actionHandler.PerformAction(
|
||||
CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior),
|
||||
target
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to bridge between new ActionDescriptor and legacy PlayerActionData
|
||||
private PlayerActionData CreateActionDataFromDescriptor(ActionDescriptor desc, ItemBehavior behavior)
|
||||
{
|
||||
// Create a temporary runtime action data
|
||||
var actionData = ScriptableObject.CreateInstance<BehaviorActionData>();
|
||||
actionData.actionName = desc.ActionName;
|
||||
actionData.duration = desc.Duration;
|
||||
actionData.animTrigger = desc.AnimTrigger;
|
||||
actionData.impactDelay = desc.ImpactDelay;
|
||||
actionData.baseSpeed = desc.AnimSpeed;
|
||||
actionData.canRepeat = desc.CanRepeat;
|
||||
actionData.behavior = behavior;
|
||||
return actionData;
|
||||
}
|
||||
|
||||
// 2. 인터랙션 (F키) - 대상이 없으면 아예 시작 안 함
|
||||
private void OnInteractTap()
|
||||
{
|
||||
@@ -250,13 +265,13 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
{
|
||||
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out var target))
|
||||
{
|
||||
if (target.TryGetComponent<MineableBlock>(out var block))
|
||||
// Use IDamageable interface instead of MineableBlock directly
|
||||
if (target.TryGetComponent<IDamageable>(out var damageable))
|
||||
{
|
||||
// 서버에서 최종 거리 검증 후 대미지 적용
|
||||
// Server-side distance validation before applying damage
|
||||
if (Vector3.Distance(transform.position, target.transform.position) <= attackRange + 1.0f)
|
||||
{
|
||||
block.TakeDamageRpc(miningDamage);
|
||||
block.PlayHitEffectClientRpc();
|
||||
damageable.TakeDamage(new DamageInfo(miningDamage, DamageType.Mining, gameObject));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,10 +502,11 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
private void HandleContinuousAction()
|
||||
{
|
||||
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||
if (selectedItem == null || !selectedItem.isTool || selectedItem.toolAction == null) return;
|
||||
if (selectedItem == null || selectedItem.behavior == null) return;
|
||||
|
||||
// [핵심] 반복 가능한 액션일 때만 Update에서 재실행
|
||||
if (selectedItem.toolAction.canRepeat)
|
||||
// Only repeat if action supports it
|
||||
var actionDesc = selectedItem.GetUseAction();
|
||||
if (actionDesc != null && actionDesc.CanRepeat)
|
||||
{
|
||||
TryExecuteAction();
|
||||
}
|
||||
@@ -501,15 +517,23 @@ public class PlayerNetworkController : NetworkBehaviour
|
||||
if (_actionHandler.IsBusy) return;
|
||||
|
||||
ItemData selectedItem = _inventory.GetSelectedItemData();
|
||||
if (selectedItem != null && selectedItem.isTool && selectedItem.toolAction != null)
|
||||
if (selectedItem == null || selectedItem.behavior == null) return;
|
||||
|
||||
var actionDesc = selectedItem.GetUseAction();
|
||||
if (actionDesc == null) return;
|
||||
|
||||
// Skip if non-repeatable action already executed once
|
||||
if (!actionDesc.CanRepeat && _hasExecutedOnce) return;
|
||||
|
||||
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
|
||||
|
||||
if (selectedItem.CanUse(gameObject, target))
|
||||
{
|
||||
// 단발성 액션인데 이미 한 번 실행했다면 스킵
|
||||
if (!selectedItem.toolAction.canRepeat && _hasExecutedOnce) return;
|
||||
|
||||
GameObject target = _lastHighlightedBlock != null ? _lastHighlightedBlock.gameObject : null;
|
||||
_actionHandler.PerformAction(selectedItem.toolAction, target);
|
||||
|
||||
_hasExecutedOnce = true; // 실행 기록 저장
|
||||
_actionHandler.PerformAction(
|
||||
CreateActionDataFromDescriptor(actionDesc, selectedItem.behavior),
|
||||
target
|
||||
);
|
||||
_hasExecutedOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user