몬스터 오류 수정 완

This commit is contained in:
윤기주_playm 2026-02-21 15:14:24 +09:00
parent 2a033581a0
commit a4f9088794
17 changed files with 635 additions and 159 deletions

Binary file not shown.

Binary file not shown.

View File

@ -9116,7 +9116,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!114 &85056389
MonoBehaviour:
m_ObjectHideFlags: 0
@ -65956,7 +65956,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!114 &600231663
MonoBehaviour:
m_ObjectHideFlags: 0
@ -204277,7 +204277,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
m_IsActive: 1
--- !u!114 &1927423249
MonoBehaviour:
m_ObjectHideFlags: 0

View File

@ -12,7 +12,7 @@ GameObject:
- component: {fileID: 5083921884124959952}
- component: {fileID: 8260729899295208043}
- component: {fileID: 3104871206233954959}
- component: {fileID: 4219865273348368139}
- component: {fileID: 3591254528184074799}
m_Layer: 6
m_Name: MonsterHP_Canvas
m_TagString: Untagged
@ -106,7 +106,7 @@ MonoBehaviour:
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &4219865273348368139
--- !u!114 &3591254528184074799
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -115,10 +115,12 @@ MonoBehaviour:
m_GameObject: {fileID: 1878191124198308913}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
m_Script: {fileID: 11500000, guid: 1ac18100e1f3bbd4d95b5aefec8b645e, type: 3}
m_Name:
m_EditorClassIdentifier:
targetObject: {fileID: 0}
targetObject: {fileID: 6300812397144094342}
hpFilledImage: {fileID: 6625992182938873674}
hpText: {fileID: 8364788239652814931}
--- !u!1 &2092223000242526199
GameObject:
m_ObjectHideFlags: 0
@ -913,14 +915,7 @@ PrefabInstance:
objectReference: {fileID: 9100000, guid: 6c71df38704934aa1940c7c9f7d5832c, type: 2}
- target: {fileID: 9500000, guid: 74236a73e891a4540a264993ac8d337b, type: 3}
propertyPath: m_WarningMessage
value: "\nBinding warning: Some generic clip(s) animate transforms that are
already bound by a Humanoid avatar. These transforms can only be changed
by Humanoid clips.\n\tTransform 'jaw_2'\n\tTransform 'thigh_l'\n\tTransform
'jaw'\n\tTransform 'pelvis'\n\tTransform 'thigh_r'\n\tTransform 'index_03_l'\n\tTransform
'lowerarm_l'\n\tTransform 'pinky_03_l'\n\tTransform 'neck_01'\n\tTransform
'pinky_03_r'\n\tand more ...\n\tFrom animation clip 'spawn'\n\tFrom animation
clip 'damage'\n\tFrom animation clip 'die'\n\tFrom animation clip 'idle'\n\tFrom
animation clip 'run'\n\tFrom animation clip 'walk'"
value:
objectReference: {fileID: 0}
- target: {fileID: 9500000, guid: 74236a73e891a4540a264993ac8d337b, type: 3}
propertyPath: m_ApplyRootMotion
@ -1006,14 +1001,14 @@ MonoBehaviour:
hitSound: {fileID: 0}
deathSound: {fileID: 0}
deathEffectPrefab: {fileID: 0}
hitEffect: {fileID: 0}
hitEffect: {fileID: 19827894, guid: 9edc33f62cf3ae849badc4cc12fe5f8a, type: 3}
impactSpawnPoint: {fileID: 0}
explodeRange: 13.07
triggerRange: 1.5
fuseTime: 3
explosionDamage: 500
explosionEffectPrefab: {fileID: 0}
fuseEffect: {fileID: 0}
fuseEffect: {fileID: 19827894, guid: 9edc33f62cf3ae849badc4cc12fe5f8a, type: 3}
fuseSound: {fileID: 0}
explosionSound: {fileID: 0}
runAnim: 'scavenger_run '

View File

@ -99,7 +99,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
lifetime: 5
hitEffectPrefab: {fileID: 0}
hitEffectPrefab: {fileID: 149626, guid: 2aca4c37fa723b4489a42288a1356056, type: 3}
destroyOnHit: 1
--- !u!135 &661439740501535806
SphereCollider:

View File

@ -424,7 +424,7 @@ GameObject:
- component: {fileID: 1900671093442362138}
- component: {fileID: 8616576226391706942}
- component: {fileID: 8938442300426676634}
- component: {fileID: 4958734649322249725}
- component: {fileID: 5242184969474340357}
m_Layer: 6
m_Name: MonsterHP_Canvas
m_TagString: Untagged
@ -518,7 +518,7 @@ MonoBehaviour:
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &4958734649322249725
--- !u!114 &5242184969474340357
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -527,10 +527,12 @@ MonoBehaviour:
m_GameObject: {fileID: 8076719585448199079}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
m_Script: {fileID: 11500000, guid: 1ac18100e1f3bbd4d95b5aefec8b645e, type: 3}
m_Name:
m_EditorClassIdentifier:
targetObject: {fileID: 0}
targetObject: {fileID: 7037573943750301576}
hpFilledImage: {fileID: 5890577574158001365}
hpText: {fileID: 3501248421892344481}
--- !u!1001 &7037573943750398246
PrefabInstance:
m_ObjectHideFlags: 0
@ -829,12 +831,7 @@ PrefabInstance:
objectReference: {fileID: 0}
- target: {fileID: 9500000, guid: 3c2d7f13adf114e5e917e1f1ec0dfe43, type: 3}
propertyPath: m_WarningMessage
value: "\nBinding warning: Some generic clip(s) animate transforms that are
already bound by a Humanoid avatar. These transforms can only be changed
by Humanoid clips.\n\tTransform 'jaw_1'\n\tTransform 'pelvis'\n\tTransform
'index_03_l'\n\tTransform 'lowerarm_l'\n\tTransform 'neck_01'\n\tTransform
'ring_02_l'\n\tTransform 'hand_l'\n\tTransform 'index_03_r'\n\tTransform
'jaw_1'\n\tTransform 'ring_03_r'\n\tand more ...\n\tFrom animation clip 'spawn'"
value:
objectReference: {fileID: 0}
- target: {fileID: 9500000, guid: 3c2d7f13adf114e5e917e1f1ec0dfe43, type: 3}
propertyPath: m_ApplyRootMotion
@ -889,6 +886,7 @@ MonoBehaviour:
Monster_Idle: Run-idle
Monster_GetDamage: Run-gethit
Monster_Die: Run-die
hitRecoverFallback: 1
attackRestDuration: 1.5
detectionRange: 10
showDebugGizmos: 1
@ -905,6 +903,7 @@ MonoBehaviour:
chargeAnim: Run-run
prepareAnim: Run-wait
walkAnim: Run-walk
showChargeDebugLog: 1
--- !u!54 &8964470652111950350
Rigidbody:
m_ObjectHideFlags: 0

View File

@ -1282,7 +1282,7 @@ GameObject:
- component: {fileID: 8545402927441503032}
- component: {fileID: 6695977629727991124}
- component: {fileID: 2866660601975138627}
- component: {fileID: 2532056432622564957}
- component: {fileID: 229555066434838198}
m_Layer: 6
m_Name: MonsterHP_Canvas
m_TagString: Untagged
@ -1376,7 +1376,7 @@ MonoBehaviour:
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &2532056432622564957
--- !u!114 &229555066434838198
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -1385,10 +1385,12 @@ MonoBehaviour:
m_GameObject: {fileID: 3192165992615934428}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
m_Script: {fileID: 11500000, guid: 1ac18100e1f3bbd4d95b5aefec8b645e, type: 3}
m_Name:
m_EditorClassIdentifier:
targetObject: {fileID: 0}
targetObject: {fileID: 5674935864780053661}
hpFilledImage: {fileID: 197665650599347112}
hpText: {fileID: 6986518538193087157}
--- !u!1 &3511228176158128295
GameObject:
m_ObjectHideFlags: 0
@ -1880,6 +1882,37 @@ Transform:
- {fileID: 6599583630465836384}
m_Father: {fileID: 6176598586534571958}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &5291722251081738114
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 351339871207031370}
m_Layer: 6
m_Name: EffectPos
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &351339871207031370
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5291722251081738114}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: -0.013, y: 0.923, z: 0.728}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8892907556271350198}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &5441857239707455192
GameObject:
m_ObjectHideFlags: 0
@ -1951,6 +1984,7 @@ Transform:
- {fileID: 3942939034497495350}
- {fileID: 6080021456397241365}
- {fileID: 8585626370423984187}
- {fileID: 351339871207031370}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!95 &186042788264501781
@ -1998,12 +2032,12 @@ MonoBehaviour:
hitRecoverFallback: 1
attackRestDuration: 1.5
detectionRange: 10
showDebugGizmos: 0
showDebugGizmos: 1
hitSound: {fileID: 0}
deathSound: {fileID: 0}
deathEffectPrefab: {fileID: 0}
hitEffect: {fileID: 0}
impactSpawnPoint: {fileID: 0}
hitEffect: {fileID: 19820634, guid: 2d051abfebeda054eac2b6a15edf6d4e, type: 3}
impactSpawnPoint: {fileID: 351339871207031370}
attackRange: 2
attackDelay: 1.5
dropItemPrefabs:

View File

@ -1503,8 +1503,7 @@ Animator:
'index_03_l'\n\tTransform 'lowerarm_l'\n\tTransform 'pinky_03_l'\n\tTransform
'neck_01'\n\tTransform 'pinky_03_r'\n\tTransform 'clavicle_l'\n\tTransform 'ring_02_l'\n\tand
more ...\n\tFrom animation clip 'spawn'\n\tFrom animation clip 'idle'\n\tFrom
animation clip 'walk'\n\tFrom animation clip 'Monster_GetDamage'\n\tFrom animation
clip 'Monster_Die'\n\tFrom animation clip 'run'"
animation clip 'run'"
m_HasTransformHierarchy: 1
m_AllowConstantClipSamplingOptimization: 1
m_KeepAnimatorStateOnDisable: 0
@ -2505,7 +2504,7 @@ GameObject:
- component: {fileID: 2977678645291439387}
- component: {fileID: 6016741491630227145}
- component: {fileID: 1054848692288420745}
- component: {fileID: 3002808900780006287}
- component: {fileID: 6148458972729052386}
m_Layer: 6
m_Name: MonsterHP_Canvas
m_TagString: Untagged
@ -2521,7 +2520,7 @@ RectTransform:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7153040190580437744}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0.060001373}
m_LocalPosition: {x: 0, y: 0, z: 0.85}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@ -2533,7 +2532,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 2.743}
m_AnchoredPosition: {x: -0, y: 0.812}
m_SizeDelta: {x: 1, y: 1}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!223 &2977678645291439387
@ -2599,7 +2598,7 @@ MonoBehaviour:
m_BlockingMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &3002808900780006287
--- !u!114 &6148458972729052386
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -2608,10 +2607,12 @@ MonoBehaviour:
m_GameObject: {fileID: 7153040190580437744}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9eb5b341f7503494e9b0f88cc4b1c17d, type: 3}
m_Script: {fileID: 11500000, guid: 1ac18100e1f3bbd4d95b5aefec8b645e, type: 3}
m_Name:
m_EditorClassIdentifier:
targetObject: {fileID: 0}
targetObject: {fileID: 3587750552762439828}
hpFilledImage: {fileID: 7307672912510681016}
hpText: {fileID: 2164660505891504432}
--- !u!1 &7576300459429704252
GameObject:
m_ObjectHideFlags: 0
@ -3559,6 +3560,10 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6725208262828196419, guid: 3fe90e2c4622f754381371a281472296, type: 3}
propertyPath: m_Materials.Array.data[0]
value:
objectReference: {fileID: 2100000, guid: aea925dd0ba5758438a4e26ced28244e, type: 2}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []

View File

@ -14583,6 +14583,35 @@ MonoBehaviour:
serializedVersion: 2
m_Bits: 64
arrowTip: {fileID: 0}
baseDamage: 10
fireEffect:
enabled: 1
bonusDamage: 5
dotDamagePerTick: 3
dotDuration: 4
dotTickInterval: 0.5
effectStrength: 1
iceEffect:
enabled: 0
bonusDamage: 2
dotDamagePerTick: 0
dotDuration: 3
dotTickInterval: 1
effectStrength: 0.5
poisonEffect:
enabled: 0
bonusDamage: 1
dotDamagePerTick: 2
dotDuration: 6
dotTickInterval: 1
effectStrength: 1
lightningEffect:
enabled: 0
bonusDamage: 8
dotDamagePerTick: 0
dotDuration: 1.5
dotTickInterval: 1
effectStrength: 1.5
showDebugRay: 1
--- !u!1 &166048
GameObject:

View File

@ -3,7 +3,8 @@ using UnityEngine.AI; // 내비게이션 기능을 불러올거에요 -> UnityEn
using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를
/// <summary>
/// 화상, 독, 슬로우 등의 상태이상을 전담 처리하는 클래스
/// 화상, 독, 슬로우, 감전 등의 상태이상을 전담 처리하는 클래스
/// [UPGRADED] 틱 간격(tickInterval) 지원 오버로드 + ApplyPoison 추가
/// </summary>
public class StatusEffectProcessor // 클래스를 선언할거에요 -> 상태이상 로직을 담당하는 클래스를
{
@ -20,36 +21,136 @@ public class StatusEffectProcessor // 클래스를 선언할거에요 -> 상태
_animator = animator; // 값을 저장할거에요 -> 애니메이터를
}
public void ApplyBurn(float damage, float duration) => _owner.StartCoroutine(BurnRoutine(damage, duration)); // 함수를 선언할거에요 -> 화상 코루틴 시작을
public void ApplySlow(float amount, float duration) => _owner.StartCoroutine(SlowRoutine(amount, duration)); // 함수를 선언할거에요 -> 슬로우 코루틴 시작을
public void ApplyShock(float damage, float duration) { _damageable.TakeDamage(damage); _owner.StartCoroutine(StunRoutine(duration)); } // 함수를 선언할거에요 -> 충격과 스턴 시작을
// ─────────────────────────────────────────────────────────
// 화상 (Burn)
// ─────────────────────────────────────────────────────────
private IEnumerator BurnRoutine(float damage, float duration) // 코루틴 함수를 정의할거에요 -> 화상 로직을
/// <summary>
/// [기존] 화상 — 2-파라미터 (하위 호환, 기본 틱 간격 0.5초)
/// </summary>
public void ApplyBurn(float damage, float duration) // 함수를 선언할거에요 -> 화상 적용을 (2-파라미터 버전)
{
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을
while (elapsed < duration) // 반복할거에요 -> 지속 시간 동안
{
_damageable.TakeDamage(damage * 0.5f); // 실행할거에요 -> 0.5초치 데미지를
yield return new WaitForSeconds(0.5f); // 기다릴거에요 -> 0.5초를
elapsed += 0.5f; // 값을 더할거에요 -> 경과 시간에
}
_owner.StartCoroutine(DotRoutine("화상", damage, duration, 0.5f)); // 실행할거에요 -> 기본 틱 간격 0.5초로 도트 코루틴을
}
private IEnumerator SlowRoutine(float amount, float duration) // 코루틴 함수를 정의할거에요 -> 슬로우 로직을
/// <summary>
/// [NEW] 화상 — 3-파라미터 (틱 간격 지정 가능)
/// </summary>
public void ApplyBurn(float damagePerTick, float duration, float tickInterval) // 함수를 선언할거에요 -> 화상 적용을 (3-파라미터 버전)
{
_owner.StartCoroutine(DotRoutine("화상", damagePerTick, duration, tickInterval)); // 실행할거에요 -> 지정 틱 간격으로 도트 코루틴을
}
// ─────────────────────────────────────────────────────────
// 독 (Poison)
// ─────────────────────────────────────────────────────────
/// <summary>
/// [NEW] 독 — 2-파라미터 (기본 틱 간격 1초)
/// </summary>
public void ApplyPoison(float damage, float duration) // 함수를 선언할거에요 -> 독 적용을 (2-파라미터 버전)
{
_owner.StartCoroutine(DotRoutine("독", damage, duration, 1f)); // 실행할거에요 -> 기본 틱 간격 1초로 도트 코루틴을
}
/// <summary>
/// [NEW] 독 — 3-파라미터 (틱 간격 지정 가능)
/// </summary>
public void ApplyPoison(float damagePerTick, float duration, float tickInterval) // 함수를 선언할거에요 -> 독 적용을 (3-파라미터 버전)
{
_owner.StartCoroutine(DotRoutine("독", damagePerTick, duration, tickInterval)); // 실행할거에요 -> 지정 틱 간격으로 도트 코루틴을
}
// ─────────────────────────────────────────────────────────
// 슬로우 (Slow)
// ─────────────────────────────────────────────────────────
/// <summary>
/// [기존] 슬로우 — 2-파라미터
/// amount: 감속 비율 (0~1 사이 값 또는 0~100 사이 값 모두 지원)
/// </summary>
public void ApplySlow(float amount, float duration) // 함수를 선언할거에요 -> 슬로우 적용을
{
_owner.StartCoroutine(SlowRoutine(amount, duration)); // 실행할거에요 -> 슬로우 코루틴을
}
// ─────────────────────────────────────────────────────────
// 감전/스턴 (Shock)
// ─────────────────────────────────────────────────────────
/// <summary>
/// [기존] 감전 — 즉발 데미지 + 스턴
/// </summary>
public void ApplyShock(float damage, float duration) // 함수를 선언할거에요 -> 감전 적용을
{
_damageable.TakeDamage(damage); // 실행할거에요 -> 즉발 데미지를
_owner.StartCoroutine(StunRoutine(duration)); // 실행할거에요 -> 스턴 코루틴을
}
// ─────────────────────────────────────────────────────────
// 코루틴들
// ─────────────────────────────────────────────────────────
/// <summary>
/// [NEW] 통합 도트 데미지 코루틴 — 화상/독 모두 이것을 사용
/// damagePerTick: 1틱당 데미지
/// duration: 총 지속 시간 (초)
/// tickInterval: 틱 간격 (초)
/// </summary>
private IEnumerator DotRoutine(string effectName, float damagePerTick, float duration, float tickInterval) // 코루틴을 정의할거에요 -> 통합 도트 데미지 로직을
{
if (tickInterval <= 0f) tickInterval = 0.5f; // 안전장치를 넣을거에요 -> 틱 간격이 0 이하면 0.5초로
float elapsed = 0f; // 변수를 초기화할거에요 -> 경과 시간을 0으로
Debug.Log($"[StatusEffect] {effectName} 시작! 틱당:{damagePerTick} 간격:{tickInterval}초 지속:{duration}초"); // 로그를 출력할거에요 -> 효과 시작 안내를
while (elapsed < duration) // 반복할거에요 -> 지속 시간이 끝날 때까지
{
_damageable.TakeDamage(damagePerTick); // 실행할거에요 -> 1틱 데미지를
yield return new WaitForSeconds(tickInterval); // 기다릴거에요 -> 틱 간격만큼
elapsed += tickInterval; // 값을 더할거에요 -> 경과 시간에 틱 간격을
}
Debug.Log($"[StatusEffect] {effectName} 종료!"); // 로그를 출력할거에요 -> 효과 종료 안내를
}
/// <summary>
/// [기존] 슬로우 코루틴
/// amount가 1 이하면 비율로(0.5 = 50% 감속), 1 초과면 퍼센트로(50 = 50% 감속)
/// </summary>
private IEnumerator SlowRoutine(float amount, float duration) // 코루틴을 정의할거에요 -> 슬로우 로직을
{
if (_agent == null) yield break; // 조건이 맞으면 종료할거에요 -> 에이전트가 없으면
float orgSpeed = _agent.speed; // 값을 저장할거에요 -> 원래 속도를
_agent.speed *= (1f - Mathf.Clamp01(amount / 100f)); // 값을 바꿀거에요 -> 속도를 줄여서
// amount 해석: 1 이하면 비율(0.5 = 50% 감속), 1 초과면 퍼센트(50 = 50% 감속)
float slowRate = amount > 1f ? Mathf.Clamp01(amount / 100f) : Mathf.Clamp01(amount); // 값을 계산할거에요 -> 감속 비율을
_agent.speed *= (1f - slowRate); // 값을 바꿀거에요 -> 속도를 줄여서
Debug.Log($"[StatusEffect] 슬로우! 원래:{orgSpeed} → 감속:{_agent.speed} ({slowRate * 100}% 감속) {duration}초간"); // 로그를 출력할거에요 -> 슬로우 내역을
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 지속 시간만큼
_agent.speed = orgSpeed; // 값을 복구할거에요 -> 원래 속도로
Debug.Log($"[StatusEffect] 슬로우 해제! 속도 복구: {orgSpeed}"); // 로그를 출력할거에요 -> 해제 안내를
}
private IEnumerator StunRoutine(float duration) // 코루틴 함수를 정의할거에요 -> 스턴 로직을
/// <summary>
/// [기존] 스턴 코루틴 — 이동 정지 + 애니 멈춤
/// </summary>
private IEnumerator StunRoutine(float duration) // 코루틴을 정의할거에요 -> 스턴 로직을
{
if (_agent != null) _agent.isStopped = true; // 명령을 내릴거에요 -> 이동 정지를
if (_animator != null) _animator.speed = 0; // 값을 바꿀거에요 -> 애니 속도를 0으로
Debug.Log($"[StatusEffect] 스턴! {duration}초간 행동 불능"); // 로그를 출력할거에요 -> 스턴 안내를
yield return new WaitForSeconds(duration); // 기다릴거에요 -> 스턴 시간만큼
if (_agent != null && _agent.isOnNavMesh) _agent.isStopped = false; // 명령을 내릴거에요 -> 이동 재개를
if (_animator != null) _animator.speed = 1; // 값을 바꿀거에요 -> 애니 속도 복구를
Debug.Log($"[StatusEffect] 스턴 해제!"); // 로그를 출력할거에요 -> 해제 안내를
}
}

View File

@ -69,7 +69,7 @@ public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래
agent = GetComponent<NavMeshAgent>(); // 가져올거에요 -> 에이전트를
audioSource = GetComponent<AudioSource>(); // 가져올거에요 -> 오디오 소스를
mobRenderer = GetComponentInChildren<Renderer>(); // 가져올거에요 -> 렌더러를
// statusProcessor = GetComponent<StatusEffectProcessor>(); // 가져올거에요 -> 상태이상 처리기를
statusProcessor = new StatusEffectProcessor(this, this, agent, animator); // 생성할거에요 -> 상태이상 처리기를 (this = 코루틴 주체 + IDamageable)
if (agent != null) agent.speed = moveSpeed; // 설정할거에요 -> 이동 속도를
@ -352,15 +352,45 @@ public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래
// 상태이상
// ─────────────────────────────────────────────────────────
public void ApplyStatusEffect(StatusEffectType type, float dmg, float dur) // 함수를 선언할거에요 -> 상태이상 적용을
/// <summary>
/// [기존] 상태이상 적용 — 3-파라미터 (하위 호환)
/// </summary>
public void ApplyStatusEffect(StatusEffectType type, float dmg, float dur) // 함수를 선언할거에요 -> 상태이상 적용을 (3-파라미터 버전)
{
ApplyStatusEffect(type, dmg, dur, 0.5f, 1f); // 실행할거에요 -> 5-파라미터 버전을 기본 틱간격 0.5초, 강도 1로
}
/// <summary>
/// [NEW] 상태이상 적용 — 5-파라미터 (Arrow 인스펙터 연동)
/// tickInterval: 도트 데미지 틱 간격 (초)
/// strength: 특수 효과 강도 (슬로우 비율, 스턴 시간 등)
/// </summary>
public void ApplyStatusEffect(StatusEffectType type, float dotDmg, float dur, float tickInterval, float strength) // 함수를 선언할거에요 -> 상태이상 적용을 (5-파라미터 버전)
{
if (isDead) return; // 중단할거에요 -> 죽었으면
switch (type)
if (statusProcessor == null) // 조건이 맞으면 실행할거에요 -> 상태이상 처리기가 없으면
{
case StatusEffectType.Burn: statusProcessor.ApplyBurn(dmg, dur); break; // 적용할거에요 -> 화상을
case StatusEffectType.Slow: statusProcessor.ApplySlow(dmg, dur); break; // 적용할거에요 -> 슬로우를
case StatusEffectType.Poison: statusProcessor.ApplyBurn(dmg, dur); break; // 적용할거에요 -> 독을
case StatusEffectType.Shock: statusProcessor.ApplyShock(dmg, dur); break; // 적용할거에요 -> 충격을
Debug.LogWarning($"[MonsterClass] {gameObject.name}에 StatusEffectProcessor가 없습니다!"); // 경고를 출력할거에요 -> 누락 안내를
return; // 중단할거에요 -> 함수를
}
switch (type) // 분기할거에요 -> 상태이상 타입에 따라
{
case StatusEffectType.Burn: // 조건이 맞으면 실행할거에요 -> 화상이라면
statusProcessor.ApplyBurn(dotDmg, dur, tickInterval); // 적용할거에요 -> 화상을 (틱 간격 포함)
break; // 분기를 종료할거에요
case StatusEffectType.Slow: // 조건이 맞으면 실행할거에요 -> 슬로우라면
statusProcessor.ApplySlow(strength, dur); // 적용할거에요 -> 슬로우를 (strength = 감속 비율)
break; // 분기를 종료할거에요
case StatusEffectType.Poison: // 조건이 맞으면 실행할거에요 -> 독이라면
statusProcessor.ApplyPoison(dotDmg, dur, tickInterval); // 적용할거에요 -> 독을 (틱 간격 포함)
break; // 분기를 종료할거에요
case StatusEffectType.Shock: // 조건이 맞으면 실행할거에요 -> 감전이라면
statusProcessor.ApplyShock(dotDmg, dur); // 적용할거에요 -> 감전을 (스턴)
break; // 분기를 종료할거에요
}
}

View File

@ -4,7 +4,7 @@ using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Co
/// <summary>
/// 발사된 화살 발사체 (파티클 프리팹에 부착됨)
/// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지)
/// [NEW] 속성 데미지(DoT) 시스템 추가
/// [UPGRADED] 속성 데미지를 인스펙터에서 속성별로 개별 조정 가능
/// </summary>
public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> MonoBehaviour를 상속받는 PlayerArrow를
{
@ -14,11 +14,59 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
[SerializeField] private LayerMask hitLayers; // 변수를 선언할거에요 -> 충돌 레이어인 hitLayers를
[SerializeField] private Transform arrowTip; // 변수를 선언할거에요 -> 화살 끝부분 위치인 arrowTip을
[Header("--- 기본 화살 데미지 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 기본 화살 데미지 --- 를
[Tooltip("기본 화살 데미지 (속성과 무관한 물리 데미지)")]
[SerializeField] private float baseDamage = 10f; // 변수를 선언할거에요 -> 기본 물리 데미지인 baseDamage를
[Header("--- 🔥 불 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 불 속성 섹션을
[SerializeField]
private ElementEffectData fireEffect = new ElementEffectData // 변수를 선언할거에요 -> 불 속성 데이터인 fireEffect를
{
bonusDamage = 5f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 5로
dotDamagePerTick = 3f, // 기본값을 설정할거에요 -> 틱당 화상 데미지를 3으로
dotDuration = 4f, // 기본값을 설정할거에요 -> 화상 지속시간을 4초로
dotTickInterval = 0.5f, // 기본값을 설정할거에요 -> 틱 간격을 0.5초로
effectStrength = 1f // 기본값을 설정할거에요 -> 효과 강도를 1로 (불은 순수 데미지라 참고용)
};
[Header("--- ❄️ 얼음 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 얼음 속성 섹션을
[SerializeField]
private ElementEffectData iceEffect = new ElementEffectData // 변수를 선언할거에요 -> 얼음 속성 데이터인 iceEffect를
{
bonusDamage = 2f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 2로
dotDamagePerTick = 0f, // 기본값을 설정할거에요 -> 틱 데미지를 0으로 (얼음은 슬로우 위주)
dotDuration = 3f, // 기본값을 설정할거에요 -> 슬로우 지속시간을 3초로
dotTickInterval = 1f, // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 0.5f // 기본값을 설정할거에요 -> 슬로우 비율을 0.5 (50% 감속)로
};
[Header("--- ☠️ 독 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 독 속성 섹션을
[SerializeField]
private ElementEffectData poisonEffect = new ElementEffectData // 변수를 선언할거에요 -> 독 속성 데이터인 poisonEffect를
{
bonusDamage = 1f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 1로
dotDamagePerTick = 2f, // 기본값을 설정할거에요 -> 틱당 독 데미지를 2로
dotDuration = 6f, // 기본값을 설정할거에요 -> 독 지속시간을 6초로
dotTickInterval = 1f, // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 1f // 기본값을 설정할거에요 -> 효과 강도를 1로
};
[Header("--- ⚡ 전기 속성 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> 전기 속성 섹션을
[SerializeField]
private ElementEffectData lightningEffect = new ElementEffectData // 변수를 선언할거에요 -> 전기 속성 데이터인 lightningEffect를
{
bonusDamage = 8f, // 기본값을 설정할거에요 -> 즉발 추가 데미지를 8로
dotDamagePerTick = 0f, // 기본값을 설정할거에요 -> 틱 데미지를 0으로 (전기는 스턴 위주)
dotDuration = 1.5f, // 기본값을 설정할거에요 -> 스턴 지속시간을 1.5초로
dotTickInterval = 1f, // 기본값을 설정할거에요 -> 틱 간격을 1초로
effectStrength = 1.5f // 기본값을 설정할거에요 -> 스턴 지속시간을 1.5초로
};
[Header("--- 디버그 시각화 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 디버그 시각화 --- 를
[SerializeField] private bool showDebugRay = true; // 변수를 선언할거에요 -> 디버그 레이 표시 여부인 showDebugRay를
// 화살 스탯
private float damage; // 변수를 선언할거에요 -> 데미지 damage를
// 화살 런타임 스탯 (Initialize에서 설정됨)
private float damage; // 변수를 선언할거에요 -> 최종 데미지 damage를
private float speed; // 변수를 선언할거에요 -> 속도 speed를
private float range; // 변수를 선언할거에요 -> 사거리 range를
private Vector3 startPos; // 변수를 선언할거에요 -> 시작 위치 startPos를
@ -26,10 +74,8 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
private bool isFired = false; // 변수를 초기화할거에요 -> 발사 여부 isFired를 거짓으로
private bool hasHit = false; // 변수를 초기화할거에요 -> 충돌 여부 hasHit을 거짓으로
// [NEW] 속성 데미지 시스템
private ArrowElementType elementType = ArrowElementType.None; // 변수를 초기화할거에요 -> 속성 타입 elementType을 없음으로
private float elementDamage = 0f; // 변수를 초기화할거에요 -> 속성 데미지 elementDamage를 0으로
private float elementDuration = 0f; // 변수를 초기화할거에요 -> 속성 지속 시간 elementDuration을 0으로
// 현재 적용 중인 속성 (Initialize에서 설정됨)
private ArrowElementType currentElement = ArrowElementType.None; // 변수를 초기화할거에요 -> 현재 속성을 없음으로
// Raycast 추적용
private Vector3 previousPosition; // 변수를 선언할거에요 -> 이전 위치 previousPosition을
@ -56,7 +102,7 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
}
/// <summary>
/// [MODIFIED] 초기화 — 속성 정보 포함 (7개 파라미터)
/// [UPGRADED] 초기화 — 속성 타입만 받고, 세부 수치는 인스펙터 설정값 사용
/// PlayerAttack.OnShootArrow()에서 호출됩니다.
/// </summary>
public void Initialize(
@ -64,22 +110,22 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
float arrowSpeed,
float maxRange,
Vector3 direction,
ArrowElementType element,
float elemDmg,
float elemDur) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를
ArrowElementType element) // 함수를 선언할거에요 -> 화살을 초기화하는 Initialize를 (5개 파라미터)
{
this.damage = dmg; // 값을 저장할거에요 -> 데미지를
this.speed = arrowSpeed; // 값을 저장할거에요 -> 속도를
this.range = maxRange; // 값을 저장할거에요 -> 사거리를
this.shootDirection = direction.normalized; // 값을 저장할거에요 -> 정규화된 발사 방향을
this.startPos = transform.position; // 값을 저장할거에요 -> 시작 위치를
this.previousPosition = transform.position; // 값을 저장할거에요 -> 이전 위치를 (레이캐스트용)
this.isFired = true; // 상태를 바꿀거에요 -> 발사됨 상태로
this.currentElement = element; // 값을 저장할거에요 -> 현재 속성 타입을
// [NEW] 속성 정보 저장
this.elementType = element; // 값을 저장할거에요 -> 속성 타입을
this.elementDamage = elemDmg; // 값을 저장할거에요 -> 속성 데미지를
this.elementDuration = elemDur; // 값을 저장할거에요 -> 속성 지속 시간을
// [UPGRADED] 기본 데미지 = 외부에서 받은 dmg + 속성 즉발 보너스 데미지
ElementEffectData effectData = GetElementData(element); // 데이터를 가져올거에요 -> 현재 속성에 맞는 설정값을
float bonusDmg = (effectData != null && effectData.enabled) ? effectData.bonusDamage : 0f; // 값을 계산할거에요 -> 속성 추가 즉발 데미지를
this.damage = dmg + bonusDmg; // 값을 저장할거에요 -> 기본 데미지 + 속성 보너스를 최종 데미지로
Debug.Log($"[Arrow] 초기화 — 기본:{dmg} + 속성보너스:{bonusDmg} = 최종:{this.damage} | 속성:{element}"); // 로그를 출력할거에요 -> 데미지 계산 내역을
// 발사 방향으로 회전
if (shootDirection != Vector3.zero) // 조건이 맞으면 실행할거에요 -> 방향이 유효하다면
@ -101,13 +147,42 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
Destroy(gameObject, 5f); // 파괴할거에요 -> 5초 뒤에 안전장치로
}
/// <summary>
/// [하위 호환성] 기존 7-파라미터 버전도 유지 (외부에서 직접 수치를 넘기는 경우)
/// </summary>
public void Initialize(
float dmg,
float arrowSpeed,
float maxRange,
Vector3 direction,
ArrowElementType element,
float elemDmg,
float elemDur) // 함수를 선언할거에요 -> 구버전 호환용 7-파라미터 초기화 함수를
{
Initialize(dmg, arrowSpeed, maxRange, direction, element); // 실행할거에요 -> 새 5-파라미터 초기화를 (인스펙터 값 사용)
}
/// <summary>
/// [하위 호환성] 방향 없는 3-파라미터 버전
/// </summary>
public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 초기화 함수를
public void Initialize(float dmg, float arrowSpeed, float maxRange) // 함수를 선언할거에요 -> 구버전 호환용 3-파라미터 초기화 함수를
{
Initialize(dmg, arrowSpeed, maxRange, transform.forward,
ArrowElementType.None, 0f, 0f); // 실행할거에요 -> 신버전 초기화 함수를 기본값으로
Initialize(dmg, arrowSpeed, maxRange, transform.forward, ArrowElementType.None); // 실행할거에요 -> 신버전 초기화를 기본값으로
}
/// <summary>
/// [NEW] 속성 타입에 맞는 인스펙터 설정 데이터를 반환
/// </summary>
private ElementEffectData GetElementData(ArrowElementType element) // 함수를 선언할거에요 -> 속성 데이터를 가져오는 GetElementData를
{
switch (element) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: return fireEffect; // 반환할거에요 -> 불 속성 설정값을
case ArrowElementType.Ice: return iceEffect; // 반환할거에요 -> 얼음 속성 설정값을
case ArrowElementType.Poison: return poisonEffect; // 반환할거에요 -> 독 속성 설정값을
case ArrowElementType.Lightning: return lightningEffect; // 반환할거에요 -> 전기 속성 설정값을
default: return null; // 반환할거에요 -> 없음 (일반 화살)
}
}
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를
@ -164,7 +239,7 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
}
/// <summary>
/// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가
/// [UPGRADED] 충돌 처리 — 인스펙터 설정값 기반 속성 데미지 적용
/// </summary>
private void HandleHit(Collider hitCollider, Vector3 hitPoint) // 함수를 선언할거에요 -> 충돌 결과를 처리하는 HandleHit을
{
@ -189,11 +264,11 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
if (monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터 스크립트가 있다면
{
// 1. 기본 데미지 적용
monster.TakeDamage(damage); // 실행할거에요 -> 데미지 입히기 함수를
Debug.Log($"적 명중! 기본데미지: {damage}"); // 로그를 출력할거에요 -> 명중 메시지를
// 1. 최종 데미지 적용 (기본 + 속성 보너스가 이미 합산됨)
monster.TakeDamage(damage); // 실행할거에요 -> 최종 데미지를 입히는 함수를
Debug.Log($"[Arrow] 적 명중! 최종 데미지: {damage} | 속성: {currentElement}"); // 로그를 출력할거에요 -> 명중 메시지를
// 2. [NEW] 속성 효과 적
// 2. [UPGRADED] 속성 지속 효과(DoT) 적용 — 인스펙터 설정값 사
ApplyElementEffect(monster); // 실행할거에요 -> 속성 효과 적용 함수를
}
@ -206,41 +281,67 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
}
else // 조건이 틀리면 실행할거에요 -> 그 외의 충돌이라면
{
// 기타 충돌 (Unknown)
Destroy(gameObject, 0.05f); // 파괴할거에요 -> 화살을
}
}
/// <summary>
/// [NEW] 속성 효과 적용
/// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다.
/// [UPGRADED] 속성 효과 적용 — 인스펙터에서 설정한 값을 직접 사용
/// MonsterClass에 ApplyStatusEffect(StatusEffectType, float dotDmg, float duration, float tickInterval, float strength)
/// 오버로드가 필요합니다.
/// </summary>
private void ApplyElementEffect(MonsterClass monster) // 함수를 선언할거에요 -> 속성 효과를 적용하는 ApplyElementEffect를
{
if (elementType == ArrowElementType.None || elementDamage <= 0f) return; // 조건이 맞으면 중단할거에요 -> 속성이 없거나 데미지가 없다
if (currentElement == ArrowElementType.None) return; // 조건이 맞으면 중단할거에요 -> 일반 화살이라
// MonsterClass에 ApplyStatusEffect가 있는지 확인
switch (elementType) // 분기할거에요 -> 속성 타입에 따라
ElementEffectData data = GetElementData(currentElement); // 데이터를 가져올거에요 -> 현재 속성의 인스펙터 설정값을
if (data == null || !data.enabled) return; // 조건이 맞으면 중단할거에요 -> 설정이 없거나 비활성이라면
switch (currentElement) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: // 조건이 맞으면 실행할거에요 -> 불 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Burn, elementDamage, elementDuration); // 실행할거에요 -> 화상 효과 적용을
Debug.Log($"화염 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break;
monster.ApplyStatusEffect( // 실행할거에요 -> 화상 상태이상을 적용하는 함수를
StatusEffectType.Burn, // 인자를 전달할거에요 -> 화상 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 데미지를 (인스펙터 값)
data.dotDuration, // 인자를 전달할거에요 -> 지속 시간을 (인스펙터 값)
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을 (인스펙터 값)
data.effectStrength // 인자를 전달할거에요 -> 효과 강도를 (인스펙터 값)
);
Debug.Log($"[Arrow] 🔥 화염! 틱당:{data.dotDamagePerTick} x {data.dotDuration}초 (간격:{data.dotTickInterval}초)"); // 로그를 출력할거에요 -> 화상 효과 내역을
break; // 분기를 종료할거에요
case ArrowElementType.Ice: // 조건이 맞으면 실행할거에요 -> 얼음 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Slow, elementDamage, elementDuration); // 실행할거에요 -> 슬로우 효과 적용을
Debug.Log($"빙결 효과! 슬로우 {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break;
monster.ApplyStatusEffect( // 실행할거에요 -> 슬로우 상태이상을 적용하는 함수를
StatusEffectType.Slow, // 인자를 전달할거에요 -> 슬로우 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 데미지를 (얼음은 보통 0)
data.dotDuration, // 인자를 전달할거에요 -> 슬로우 지속 시간을
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을
data.effectStrength // 인자를 전달할거에요 -> 슬로우 비율을 (0.5면 50% 감속)
);
Debug.Log($"[Arrow] ❄️ 빙결! 슬로우:{data.effectStrength * 100}% x {data.dotDuration}초"); // 로그를 출력할거에요 -> 빙결 효과 내역을
break; // 분기를 종료할거에요
case ArrowElementType.Poison: // 조건이 맞으면 실행할거에요 -> 독 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Poison, elementDamage, elementDuration); // 실행할거에요 -> 독 효과 적용을
Debug.Log($"독 효과! {elementDamage} 데미지 x {elementDuration}초"); // 로그를 출력할거에요 -> 효과 설명을
break;
monster.ApplyStatusEffect( // 실행할거에요 -> 독 상태이상을 적용하는 함수를
StatusEffectType.Poison, // 인자를 전달할거에요 -> 독 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 독 데미지를
data.dotDuration, // 인자를 전달할거에요 -> 독 지속 시간을
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을
data.effectStrength // 인자를 전달할거에요 -> 효과 강도를
);
Debug.Log($"[Arrow] ☠️ 독! 틱당:{data.dotDamagePerTick} x {data.dotDuration}초 (간격:{data.dotTickInterval}초)"); // 로그를 출력할거에요 -> 독 효과 내역을
break; // 분기를 종료할거에요
case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 번개 속성이라면
monster.ApplyStatusEffect(StatusEffectType.Shock, elementDamage, elementDuration); // 실행할거에요 -> 감전(스턴) 효과 적용을
Debug.Log($"감전 효과! {elementDamage} 범위 데미지"); // 로그를 출력할거에요 -> 효과 설명을
break;
case ArrowElementType.Lightning: // 조건이 맞으면 실행할거에요 -> 전기 속성이라면
monster.ApplyStatusEffect( // 실행할거에요 -> 감전(스턴) 상태이상을 적용하는 함수를
StatusEffectType.Shock, // 인자를 전달할거에요 -> 감전 타입을
data.dotDamagePerTick, // 인자를 전달할거에요 -> 틱당 데미지를 (전기는 보통 0)
data.dotDuration, // 인자를 전달할거에요 -> 스턴 지속 시간을
data.dotTickInterval, // 인자를 전달할거에요 -> 틱 간격을
data.effectStrength // 인자를 전달할거에요 -> 스턴 시간을
);
Debug.Log($"[Arrow] ⚡ 감전! 스턴:{data.effectStrength}초 | 추가데미지:{data.bonusDamage}"); // 로그를 출력할거에요 -> 감전 효과 내역을
break; // 분기를 종료할거에요
}
}
@ -269,6 +370,23 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
}
}
/// <summary>
/// [PUBLIC] 외부에서 인스펙터 속성 데이터를 읽을 수 있는 접근자
/// UI 등에서 속성 정보를 표시할 때 사용
/// </summary>
public ElementEffectData GetCurrentElementData() // 함수를 선언할거에요 -> 현재 속성 데이터를 반환하는 접근자를
{
return GetElementData(currentElement); // 반환할거에요 -> 현재 속성의 설정 데이터를
}
/// <summary>
/// [PUBLIC] 현재 속성 타입 반환
/// </summary>
public ArrowElementType GetCurrentElement() // 함수를 선언할거에요 -> 현재 속성 타입을 반환하는 접근자를
{
return currentElement; // 반환할거에요 -> 현재 속성 타입을
}
/// <summary>
/// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가
/// </summary>
@ -288,15 +406,15 @@ public class PlayerArrow : MonoBehaviour // 클래스를 선언할거에요 -> M
Gizmos.color = Color.yellow; // 색상을 설정할거에요 -> 노란색으로
Gizmos.DrawLine(tipPosition, tipPosition + direction * raycastDistance); // 선을 그릴거에요 -> 궤적을 표시하는
// [NEW] 속성별 색상 표시
switch (elementType) // 분기할거에요 -> 속성 타입에 따라
// 속성별 색상 표시
switch (currentElement) // 분기할거에요 -> 속성 타입에 따라
{
case ArrowElementType.Fire: Gizmos.color = Color.red; break; // 색상을 바꿀거에요 -> 빨강으로
case ArrowElementType.Ice: Gizmos.color = Color.cyan; break; // 색상을 바꿀거에요 -> 청록으로
case ArrowElementType.Poison: Gizmos.color = Color.green; break; // 색상을 바꿀거에요 -> 초록으로
case ArrowElementType.Lightning: Gizmos.color = Color.yellow; break; // 색상을 바꿀거에요 -> 노랑으로
}
if (elementType != ArrowElementType.None) // 조건이 맞으면 실행할거에요 -> 속성이 있다면
if (currentElement != ArrowElementType.None) // 조건이 맞으면 실행할거에요 -> 속성이 있다면
{
Gizmos.DrawWireSphere(transform.position, 0.5f); // 그림을 그릴거에요 -> 속성 표시용 구체를
}

View File

@ -0,0 +1,27 @@
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
/// <summary>
/// 속성별 데미지 설정을 담는 데이터 클래스
/// 인스펙터에서 속성별로 데미지, 지속시간, 틱 간격을 조정할 수 있습니다.
/// </summary>
[System.Serializable] // 인스펙터에서 보이게 할거에요 -> 이 클래스를
public class ElementEffectData // 클래스를 선언할거에요 -> 속성 효과 데이터를 담는 ElementEffectData를
{
[Tooltip("속성 활성화 여부")] // 툴팁을 표시할거에요 -> 마우스 올리면
public bool enabled = false; // 변수를 선언할거에요 -> 속성 사용 여부인 enabled를
[Tooltip("속성 추가 데미지 (기본 화살 데미지에 더해지는 즉발 추가 데미지)")]
public float bonusDamage = 5f; // 변수를 선언할거에요 -> 속성 즉발 추가 데미지인 bonusDamage를
[Tooltip("지속 데미지(DoT) 1틱당 데미지량")]
public float dotDamagePerTick = 3f; // 변수를 선언할거에요 -> 틱당 도트 데미지인 dotDamagePerTick을
[Tooltip("지속 데미지 총 지속 시간 (초)")]
public float dotDuration = 4f; // 변수를 선언할거에요 -> 도트 총 지속 시간인 dotDuration을
[Tooltip("지속 데미지 틱 간격 (초) — 예: 0.5면 0.5초마다 데미지")]
public float dotTickInterval = 0.5f; // 변수를 선언할거에요 -> 도트 틱 간격인 dotTickInterval을
[Tooltip("특수 효과 강도 (슬로우 비율, 스턴 시간 등 속성마다 다르게 활용)")]
public float effectStrength = 0.5f; // 변수를 선언할거에요 -> 특수 효과 강도인 effectStrength를
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35e52ffeb828c4a449da5a4e6638f64c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -18,15 +18,23 @@ public class Stats : MonoBehaviour // 클래스를 선언할거에요 -> MonoBeh
[Header("--- 밸런스 설정 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 밸런스 설정 --- 을
[SerializeField] private float runSpeedMultiplier = 1.5f; // 변수를 선언할거에요 -> 달리기 속도 배율인 runSpeedMultiplier를
[Header("--- 최소값 제한 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 최소값 제한 --- 을
[Tooltip("이동 속도가 이 값 아래로 내려가지 않습니다")]
[SerializeField] private float minMoveSpeed = 0.5f; // 변수를 선언할거에요 -> 최소 이동 속도인 minMoveSpeed를
[Tooltip("최대 체력이 이 값 아래로 내려가지 않습니다")]
[SerializeField] private float minMaxHealth = 1f; // 변수를 선언할거에요 -> 최소 체력인 minMaxHealth를
[Tooltip("공격력이 이 값 아래로 내려가지 않습니다")]
[SerializeField] private float minAttackDamage = 0f; // 변수를 선언할거에요 -> 최소 공격력인 minAttackDamage를
/* =========================
*
* ========================= */
public float MaxHealth => baseMaxHealth + bonusMaxHealth; // 프로퍼티를 선언할거에요 -> 총 최대 체력을 반환하는 MaxHealth를
public float BaseAttackDamage => baseAttackDamage + bonusAttackDamage; // 프로퍼티를 선언할거에요 -> 총 기본 공격력을 반환하는 BaseAttackDamage를
public float TotalAttackDamage => BaseAttackDamage + weaponDamage; // 프로퍼티를 선언할거에요 -> 최종 공격력(무기 포함)을 반환하는 TotalAttackDamage를
public float MaxHealth => Mathf.Max(baseMaxHealth + bonusMaxHealth, minMaxHealth); // 프로퍼티를 선언할거에요 -> 총 최대 체력을 최소값 이상으로 반환하는 MaxHealth를
public float BaseAttackDamage => Mathf.Max(baseAttackDamage + bonusAttackDamage, minAttackDamage); // 프로퍼티를 선언할거에요 -> 총 기본 공격력을 최소값 이상으로 반환하는 BaseAttackDamage를
public float TotalAttackDamage => Mathf.Max(BaseAttackDamage + weaponDamage, minAttackDamage); // 프로퍼티를 선언할거에요 -> 최종 공격력(무기 포함)을 최소값 이상으로 반환하는 TotalAttackDamage를
// ✨ [수정] 이제 무게 페널티 없이 순수 속도만 계산합니다.
public float CurrentMoveSpeed => baseMoveSpeed + bonusMoveSpeed; // 프로퍼티를 선언할거에요 -> 현재 이동 속도를 반환하는 CurrentMoveSpeed를
// ✨ [수정] Mathf.Max로 최소 속도 보장
public float CurrentMoveSpeed => Mathf.Max(baseMoveSpeed + bonusMoveSpeed, minMoveSpeed); // 프로퍼티를 선언할거에요 -> 현재 이동 속도를 최소값 이상으로 반환하는 CurrentMoveSpeed를
public float CurrentRunSpeed => CurrentMoveSpeed * runSpeedMultiplier; // 프로퍼티를 선언할거에요 -> 현재 달리기 속도를 반환하는 CurrentRunSpeed를
private void Update() // 함수를 실행할거에요 -> 매 프레임마다 Update를

View File

@ -1,42 +1,97 @@
using UnityEngine; // 유니티 엔진의 기본 기능을 불러올거에요 -> UnityEngine을
using System; // 기본 시스템 기능을 사용할거에요 -> System을
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
using TMPro; // TextMeshPro를 사용할거에요 -> TMPro를
public class EnemyHealth : MonoBehaviour, IDamageable // 클래스를 선언할거에요 -> MonoBehaviour와 IDamageable을 상속받는 EnemyHealth를
/// <summary>
/// 몬스터 체력바 UI — MonsterClass.OnHealthChanged 이벤트 연동
/// 몬스터 프리팹의 MonsterHP_Canvas에 부착
/// </summary>
public class MonsterHPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 몬스터 체력바 UI를
{
[Header("--- 능력치 ---")] // 인스펙터 창에 제목을 표시할거에요 -> --- 능력치 --- 를
[SerializeField] private float maxHealth = 50f; // 변수를 선언할거에요 -> 최대 체력(50.0)을 maxHealth에
private float _currentHealth; // 변수를 선언할거에요 -> 현재 체력을 저장할 _currentHealth를
[Header("--- 타겟 ---")] // 인스펙터 제목을 달거에요 -> 타겟 설정을
[Tooltip("MonsterClass가 붙어있는 몬스터 오브젝트")]
[SerializeField] private GameObject targetObject; // 변수를 선언할거에요 -> 타겟 오브젝트를 (몬스터 루트)
public event Action<float, float> OnHealthChanged; // 이벤트를 선언할거에요 -> 체력 변경 시 현재/최대를 알릴 OnHealthChanged를
public bool IsDead { get; private set; } // 프로퍼티를 선언할거에요 -> 사망 여부를 외부에서 읽기만 가능하게 IsDead에
[Header("--- UI 요소 ---")] // 인스펙터 제목을 달거에요 -> UI 요소를
[Tooltip("HP_Filled (Image, Fill 방식)")]
[SerializeField] private Image hpFilledImage; // 변수를 선언할거에요 -> 체력바 Fill 이미지를
private void Start() // 함수를 실행할거에요 -> 시작 시 Start를
[Tooltip("HP_Text (TMP) — '현재HP / 최대HP' 표시")]
[SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트를
// 캐싱된 참조
private MonsterClass _monster; // 변수를 선언할거에요 -> 몬스터 클래스 참조를
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
_currentHealth = maxHealth; // 값을 초기화할거에요 -> 현재 체력을 최대 체력으로
// ⭐ 시작하자마자 UI에 현재 숫자가 뜨도록 신호 발송
OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 초기 체력 상태를 알리기 위해
// 타겟이 비어있으면 부모에서 자동으로 찾기
if (targetObject == null) // 조건이 맞으면 실행할거에요 -> 타겟이 설정 안 됐다면
{
targetObject = transform.root.gameObject; // 할당할거에요 -> 최상위 부모 오브젝트를
}
// IDamageable 인터페이스 구현
public void TakeDamage(float amount) // 함수를 선언할거에요 -> 데미지를 입는 TakeDamage를
if (targetObject != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
{
if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면
_currentHealth = Mathf.Max(0, _currentHealth - amount); // 값을 계산할거에요 -> 체력에서 데미지를 빼고 0 밑으로 안 내려가게
// ⭐ 피격 시 로그와 함께 UI 갱신 신호 발송
Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); // 로그를 출력할거에요 -> 피격 정보와 남은 체력을
OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 변경된 체력 정보를 알리기 위해
if (_currentHealth <= 0) Die(); // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 처리 Die를
_monster = targetObject.GetComponent<MonsterClass>(); // 가져올거에요 -> MonsterClass를
if (_monster == null) // 조건이 맞으면 실행할거에요 -> 직접 못 찾았다면
{
_monster = targetObject.GetComponentInParent<MonsterClass>(); // 가져올거에요 -> 부모에서 MonsterClass를
}
}
}
private void Die() // 함수를 선언할거에요 -> 사망 처리를 하는 Die를
private void OnEnable() // 함수를 실행할거에요 -> 활성화 시 OnEnable을
{
if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽음 처리 중이라면
IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로
if (_monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터가 있다면
{
_monster.OnHealthChanged += OnMonsterHealthChanged; // 등록할거에요 -> 체력 변경 이벤트 리스너를
}
Debug.Log($"{gameObject.name} 사망!"); // 로그를 출력할거에요 -> 사망 메시지를
Destroy(gameObject, 1.5f); // 파괴할거에요 -> 이 오브젝트를 1.5초 뒤에
// 활성화 시 즉시 UI 갱신 (풀에서 재사용될 때 대비)
ResetUI(); // 실행할거에요 -> UI 초기화를
}
private void OnDisable() // 함수를 실행할거에요 -> 비활성화 시 OnDisable을
{
if (_monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터가 있다면
{
_monster.OnHealthChanged -= OnMonsterHealthChanged; // 해제할거에요 -> 체력 변경 이벤트 리스너를
}
}
/// <summary>
/// MonsterClass.OnHealthChanged(currentHP, maxHP) 이벤트 콜백
/// </summary>
private void OnMonsterHealthChanged(float currentHP, float maxHP) // 함수를 선언할거에요 -> 체력 변경 콜백을
{
// Fill Amount 갱신
if (hpFilledImage != null) // 조건이 맞으면 실행할거에요 -> Fill 이미지가 있다면
{
float ratio = (maxHP > 0) ? currentHP / maxHP : 0f; // 비율을 계산할거에요 -> 현재 / 최대로
hpFilledImage.fillAmount = Mathf.Clamp01(ratio); // 값을 설정할거에요 -> 0~1 사이로 Fill Amount를
}
// 텍스트 갱신
if (hpText != null) // 조건이 맞으면 실행할거에요 -> 텍스트가 있다면
{
hpText.text = $"{currentHP:F0} / {maxHP:F0}"; // 텍스트를 설정할거에요 -> "현재 / 최대" 형식으로
}
}
/// <summary>
/// UI를 풀 체력 상태로 초기화 (스폰/재활성화 시)
/// </summary>
private void ResetUI() // 함수를 선언할거에요 -> UI 초기화를
{
if (hpFilledImage != null) // 조건이 맞으면 실행할거에요 -> Fill 이미지가 있다면
{
hpFilledImage.fillAmount = 1f; // 값을 설정할거에요 -> Fill을 꽉 채우기로
}
// _monster가 있으면 실제 스탯으로 텍스트 갱신
if (hpText != null) // 조건이 맞으면 실행할거에요 -> 텍스트가 있다면
{
hpText.text = ""; // 텍스트를 비울거에요 -> 초기화로 (첫 OnHealthChanged에서 갱신됨)
}
}
}

View File

@ -1,36 +1,100 @@
using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을
using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를
using TMPro; // TextMeshPro를 사용할거에요 -> TMPro를
/// <summary>
/// 플레이어 체력바 UI — PlayerHealth 이벤트 연동
/// HP_Filled (Image Fill) + Text (TMP) 실시간 갱신
/// </summary>
public class HPUibar : MonoBehaviour // 클래스를 선언할거에요 -> 체력바 UI를
{
[Header("타겟")] // 인스펙터 제목을 달거에요 -> 타겟 설정을
[SerializeField] private GameObject targetObject; // 변수를 선언할거에요 -> 타겟 오브젝트를
[Header("--- 타겟 ---")] // 인스펙터 제목을 달거에요 -> 타겟 설정을
[SerializeField] private GameObject targetObject; // 변수를 선언할거에요 -> 타겟 오브젝트를 (Player)
// 컴포넌트들
private Slider _slider; // 변수를 선언할거에요 -> 슬라이더를
private IDamageable _damageable; // 변수를 선언할거에요 -> 데미지 인터페이스를
// ⭐ [수정] TrainingDummy -> DummyBot
private DummyBot _dummy; // 변수를 선언할거에요 -> 더미 봇 참조를
[Header("--- UI 요소 ---")] // 인스펙터 제목을 달거에요 -> UI 요소를
[Tooltip("HP_Filled (Image, Fill 방식)")]
[SerializeField] private Image hpFilledImage; // 변수를 선언할거에요 -> 체력바 Fill 이미지를
[Tooltip("Text (TMP) — '현재HP / 최대HP' 표시")]
[SerializeField] private TextMeshProUGUI hpText; // 변수를 선언할거에요 -> 체력 텍스트를
// 캐싱된 참조
private PlayerHealth _playerHealth; // 변수를 선언할거에요 -> 플레이어 체력 스크립트를
private Stats _playerStats; // 변수를 선언할거에요 -> 플레이어 스탯 스크립트를
private void Awake() // 함수를 실행할거에요 -> 초기화 Awake를
{
_slider = GetComponent<Slider>(); // 가져올거에요 -> 슬라이더를
if (targetObject != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면
{
_damageable = targetObject.GetComponent<IDamageable>(); // 가져올거에요 -> 인터페이스
_dummy = targetObject.GetComponent<DummyBot>(); // 가져올거에요 -> 더미 봇을
_playerHealth = targetObject.GetComponent<PlayerHealth>(); // 가져올거에요 -> PlayerHealth
_playerStats = targetObject.GetComponent<Stats>(); // 가져올거에요 -> Stats를
}
}
private void Update() // 함수를 실행할거에요 -> 매 프레임 업데이트를
private void OnEnable() // 함수를 실행할거에요 -> 활성화 시 OnEnable을
{
if (_slider != null && _dummy != null) // 조건이 맞으면 실행할거에요 -> 슬라이더와 더미가 있다면
if (_playerHealth != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면
{
// _dummy에서 체력 정보를 가져오는 방식이 필요하다면 MonsterClass의 OnHealthChanged 이벤트를 쓰는 게 좋음
// 하지만 기존 로직 유지를 위해 단순 접근 가정
// _slider.value = ...
_playerHealth.OnHealthChanged.AddListener(OnHealthRatioChanged); // 등록할거에요 -> 체력 변경 이벤트 리스너를
}
}
// (기존 코드에 있던 나머지 로직은 그대로 유지)
private void OnDisable() // 함수를 실행할거에요 -> 비활성화 시 OnDisable을
{
if (_playerHealth != null) // 조건이 맞으면 실행할거에요 -> 체력 스크립트가 있다면
{
_playerHealth.OnHealthChanged.RemoveListener(OnHealthRatioChanged); // 해제할거에요 -> 체력 변경 이벤트 리스너를
}
}
private void Start() // 함수를 실행할거에요 -> 시작 시 Start를
{
UpdateUI(); // 실행할거에요 -> 초기 UI 갱신을 (시작하자마자 올바른 값 표시)
}
/// <summary>
/// PlayerHealth.RefreshHealthUI()에서 ratio(0~1)를 받아 호출됨
/// </summary>
private void OnHealthRatioChanged(float ratio) // 함수를 선언할거에요 -> 체력 비율 변경 콜백을
{
// Fill Amount 갱신
if (hpFilledImage != null) // 조건이 맞으면 실행할거에요 -> Fill 이미지가 있다면
{
hpFilledImage.fillAmount = Mathf.Clamp01(ratio); // 값을 설정할거에요 -> 0~1 사이로 Fill Amount를
}
// 텍스트 갱신
UpdateHPText(); // 실행할거에요 -> 텍스트 갱신 함수를
}
/// <summary>
/// 텍스트를 "현재HP / 최대HP" 형식으로 갱신
/// </summary>
private void UpdateHPText() // 함수를 선언할거에요 -> HP 텍스트를 갱신하는 UpdateHPText를
{
if (hpText == null || _playerHealth == null || _playerStats == null) return; // 조건이 맞으면 중단할거에요 -> 필수 참조가 없으면
float current = _playerHealth.CurrentHP; // 값을 가져올거에요 -> 현재 체력을
float max = _playerStats.MaxHealth; // 값을 가져올거에요 -> 최대 체력을
hpText.text = $"{current:F0} / {max:F0}"; // 텍스트를 설정할거에요 -> "현재 / 최대" 형식으로
}
/// <summary>
/// 이벤트 없이 직접 UI를 갱신 (Start, 수동 호출용)
/// </summary>
private void UpdateUI() // 함수를 선언할거에요 -> 전체 UI 갱신을
{
if (_playerHealth == null || _playerStats == null) return; // 조건이 맞으면 중단할거에요 -> 참조가 없으면
float max = _playerStats.MaxHealth; // 값을 가져올거에요 -> 최대 체력을
float current = _playerHealth.CurrentHP; // 값을 가져올거에요 -> 현재 체력을
float ratio = (max > 0) ? current / max : 0f; // 비율을 계산할거에요 -> 현재/최대로
if (hpFilledImage != null) // 조건이 맞으면 실행할거에요 -> Fill 이미지가 있다면
{
hpFilledImage.fillAmount = Mathf.Clamp01(ratio); // 값을 설정할거에요 -> Fill Amount를
}
UpdateHPText(); // 실행할거에요 -> 텍스트 갱신을
}
}