From a4f90887946b8953111f6c513a255957831407cf Mon Sep 17 00:00:00 2001 From: hydrozen Date: Sat, 21 Feb 2026 15:14:24 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AA=AC=EC=8A=A4=ED=84=B0=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95=20=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .plastic/plastic.changes | Bin 2551 -> 3433 bytes .plastic/plastic.wktree | Bin 1894189 -> 1894528 bytes Assets/0.SCENE/MainGame.unity | 6 +- .../MyMonster/KamikazeMonster.prefab | 23 +- .../Throw Monster Weapon.prefab | 2 +- .../1.myPrefab/MyMonster/RushMonster2.prefab | 19 +- .../1.myPrefab/MyMonster/SwordMonster.prefab | 48 +++- .../1.myPrefab/MyMonster/ThrowMonster.prefab | 21 +- .../Fireball/FireballMissileFire 1.prefab | 29 +++ .../Core/Utils/StatusEffectProcessor.cs | 131 +++++++++-- Assets/Scripts/Enemy/AI/MonsterClass.cs | 44 +++- Assets/Scripts/Player/Combat/Arrow.cs | 210 ++++++++++++++---- .../Player/Combat/ElementEffectData.cs | 27 +++ .../Player/Combat/ElementEffectData.cs.meta | 11 + Assets/Scripts/Player/Stats/Stats.cs | 18 +- Assets/Scripts/UI/Enemy/EnemyHP Ui.cs | 109 ++++++--- Assets/Scripts/UI/HUD/HP Ui bar.cs | 96 ++++++-- 17 files changed, 635 insertions(+), 159 deletions(-) create mode 100644 Assets/Scripts/Player/Combat/ElementEffectData.cs create mode 100644 Assets/Scripts/Player/Combat/ElementEffectData.cs.meta diff --git a/.plastic/plastic.changes b/.plastic/plastic.changes index 713db5819ca3f1fe238fb69bcbe68be2c26f7b66..24469120e559e42c0e45b5c39d6a83474e9b6ebc 100644 GIT binary patch literal 3433 zcmbVO%WvaE9Ckt4cFPJ_6)i!_A&BR0q_jz!mqaDfBzBv|trO>sSjlnxw&Nsr;?^A4 zGZO3tEhn^EIB-}ltOVTQ%3hGT0OD~#`~x5%4wP|hPvS}410%(bXFUGC-}n1|GxIE^ zGKML!QMy1zEyWyjT$GnoZfym-CZ6`ky3Gv|iA?qlL1QE}&gvS5oq_f*_+c1UGr$Ej z>xK2%{ju2;Ew z^8lKkX7}mrY{mcMF#$KnH-Yxtd026cGZ|;H*nWj8E3n6Mnm0#T!5~kLc|hR4ix2XQ5V%a9lKQ|f10{3|Q9XqK!&T~2s5-6I({k&e zI#H7K!id|8ETQ=-bs4bPsEa^#4|`es1BAlBQ_vS5QW(N|>cuKXIont4Cf^V; zN~%$)p7sERs}EdvgV^U396ciEP(X9=hbOIX0v+MuD-Vkgi=?=EwXBtSy_`1dN`BOs zYVs*q!{#}__8)1lO0xyq;qj!WLbSy(Lw&Gd|KE_ctcRelJJX#Ofhw?UGq$g{+8o97 zie-Tpl6!}_p#A3-bcDG+Qz07Uu$+({Vn$_a1`KyDNacH9l1Q3nOWQ1UthZ@3PiDKZ z1le~yarU%3b;*nyw55;xp`O@BJfR|LKu-*y8u_eQN*z_G3c+W)iIc5vG8wB_ySwpw zVV*dL0_q7JkwQawKq-EQQjDl+EqBmuW^DOL9<`H2(#SIbPat)9?U2zVg|WF%H3&qF zw-6Rk4GOAJ77_$7BR;_^hEy$*;&f2bfH%H*7bYZFA=l4mfeKPZ1i6J!fr5Mu1tCmp z*h-8hq{3Bd%}TOpSeeO13)Mz#Vu+p?$z^-;2n8XG!Vi#wxSvYXHa}4nBUK}EGH(mL zDWGuo9jL~XpPM@sb~DXx@;-~d|J z^+6PGFRT{#kk#USWV3{6xZ@3Ak=#yZOYEy{nN<@t_6XNIT!#YKt^&?=!Iw_Sth!q? zJa&MtM+oc}$Zi2)zw`#c!fT6FTolCaI5*H6iI~_tv^t;C}24 zU=ejhS}|N=Px_gz*%#XlVw2tp?u~P!IU=v&(wOTps=AQeGX(dSdC&X)+@ U=ujt9p#!*ltF6xFF6{;X0q2iSVE_OC literal 2551 zcmbVOO^@3|7R6!j^@7d*^2 z_Ez_oR#%F8*}hwEaUgf>H^HTFG@tdVW-)uJhnadGiF9@}x_{sP9^XFv<>m($lV7jn z#{mB9s=b5H<6sLN@Z zgtSlt@EoQTa(RUqj+e>ub}gQGXnp_Rw7R|cayjfw+c1AL)Aff4DS-AhZ8uj*F5~ltmsCD@>tm7^Y7>h^uzHoFYev0#3?-z{-gvbb$XN z0Ui#p6i$&B0qk$nea1bzyFFd6i z`x#nu4ZU1ulp&pv#zIkyHKD+u(s}X3M?jM7Ze!6vP&a_K2K>kaoO;{R#7WKFzlp9b zR*+R%>2bGmGLz#idJ3YMcwa_-o+0bpZiyN)ZS2V8O7580q-U+{)DXVoL*aL)xdd5R zk0$e#qjs-4AM!c}3Z2#hQ~2jwtEz$Z7D86L&6-FPd?`Q=1;vlTqo&1;3J%f|V5dD+ zA0_HlGb~LE7G^Mo{l`8O?$kI{lDQ@DWx;q@yzj^2M^}j4@$`HWFZQKllx?*^y(XYO zjZN^=Cl=2QA#SB24as`ErM;a&VLTA<7VkH804GjVY$xJ3q}UDLvGt?vR@L>OgW%6U>=ZT<$&QNXaM1W z1)2ohR*u>H>|t}^R|Se)B}h@hVd9wYrcUCe_J|)b^>RNJJI`-pb8W?j2xy;NA9>D? zCw|U<)dh_iAej}SwUbIS)tgsjaZ*#ami`4N7f#^r)XKtXK93Krc%$UET;vks*&1y| z6YW_gFJ#8)*?ckHmhPJ)BK{dUx;O)AXFP(2h7<*8`0teS=%|7B$mge5IW^w(;q;qp fh($wJy1c;Gnv=BHf^@-5nNi%0et6xQx?#g-wmX-D diff --git a/.plastic/plastic.wktree b/.plastic/plastic.wktree index 0e6f65a0e8dee9084a64aa8463be9800e18f07dc..2b307fceedd409b12fa2767dcfe9fc7a120e1372 100644 GIT binary patch delta 3742 zcmb7Hdu&tZ6~Fg$a0yu<8IS8YPAZ^*G;VMnIQAHB>%@=Pv9X=t_|c^F+K%78wsRfF z$+CvWN?6+wAkc4B3#$s0l>%MI<4Uw_Dr^<|W6*BWq^)SQS}QhHMVquqY?{VC`#x}- zde*LVbme=`Iltfeedm1l^Vu8p&X4H>^?1!esYgCQV})yC?Xtohus<;ocdy~@*bk_K z$K+N2dYL)%?yoPdmJgQ188@(wi{kcE^Jmwvf%CW4$Yxr!kvGny{nv8PHfY~?Vz-CRt?q_`N{nXh5~yF|lG`kIral1)<=C(Fclz)(ACK3g(1{FZY2* zzz7W)l;=SaCn@-8A78`@6KDc{8Yl;_IJgHZx-gJ5;lhEX*gmo zCjP|#OPslA1{ZO#orZBsF|9cY9;$%0_@Y83DxoZXPm4Z3Z88r!xh~=)5 zPK}o9)Wa0K7vlMD41+^BJUIty;jbbKn#-d&D6a}d?4{|gOUc?{OTzWqHP(Cc84D zh-cD@OgAMT@J-6ylzIZmssCPeK^6=%mo(u$ihZ%D3I8IDQl+H)w~5hg5q;hw_m&{+ zD>q9=DQNNYwSLDh>Xwrwp-0P2$aMWt z0v93;Z}Kk8e*&WSLD$VM*Re|4T@mM$zppDhb&C= zJWUd8=P|!0iLFxLqaiU}?DGc-J~hqxz~nR`1wk6VxWMIH5?@C&z-r672E#UIz)ud^b!gJYRJ^9!XgKv8QS=p6bM1*$4CdAR&2z*`FmvMj zr@uI!$L!i1SE^lOp0R9dFy5t{RO%+eF@!n3gU7U$G+k>q`)1KZd|TjBDJee@FuIHQ zWt;r7KWhs0cbgof&ERqSsQ#EDsmgTGep3!{aZ7nFb{Y=vF0Oy8fLSS+-|;T&CO&Ly z`Dd580O_(i9i3WVE})KBbCcdaYs%Z>LL0&X84u||N1{E&kQxE9QZO(1{XK+M0XJTi z)#A0b>ugjP&QSSHaBf%WK(v$ZYS7tE?Wqzjt)A!`hQn zX0=`)TEN;vJn8-tE+OvsA{p{W+x&$N02Pwr6nDzzPsDkW%UxtVwuz)7iA{;o45=M? zXJg^}83|{s{vq0$99NE8C&|uOnl{GKPWir2nWcnbxRfCz6VjC%E9Bw2Ed|M@DjBo) Js_fLG{{?$c!Dj#f delta 2966 zcma)8drVvR6~@QlV5kc>gj|EYG?}4FV2p>sK#P+)5F3MuF~%UQIOB3xzq{>3`M^|ZG$hN9lx~(Kznnml=Aq|5QK!eXbcd8Wq|IDVA zZvG&jnoV-k;L0Ts!+?4I{N_^BPw_>#b~Pt`{@&)ChW9_6SXc=BE>m3P^trqyy{lZ# z)a(6fL&ZKvsotPe7Dg$_yTTg%8fVu>j+H%Wig~0y0_UbF>HLedsKKE#U5wDFEP92n zUnQ?F=xoES)#@>)!l!U6mD&VdZ4zC3l&(8Lm+}ZSe4H)4P%2jO%Cekqun|5>pGWR! z>2hf6Z5mggbTr)OZgY(&m5=h`gmu>WR|>Y^>Ln1$dJ-flGeox>ruhGoKd6_#R6t&l)MO_^AZ* zI!EBGKS~O4VTqS>AXGdPD(11FVz|5wUShC91JN2FU`Cq;O7RPoK!kG|*aWXKxX78G z2!IeOEw~ytE4U`458Ay<-RJLUkSjoAwUxVSbat(4ta)#j&BF|N#~MiiJL2t1Q(AFn zGdXSl6lqw4Ire|*K&+h3l;W$+K*4JavRr5+ktz$WHOKQ5YayTdC`rF23{DaK=3CLK zC62p(Dcy)KYvFj=z=5>3ubAylEB3Sy%C$+7t@0~m`xpJgQubO4Eb`z8g9ADe%Si(U z&G@n|mM;6A4o>92JcGXOxIJjbW8K7ldPy1`Gj_Ebe$Ow284l@GkSW@@;qticR#s_Y$ zdtnf<6&`q=m#L0P5Ldjgm7lf*_y;e9DNT|xORzFP+W#i7hx*~NfLcrdwiIHpxUJije(-6;JILzh$Hn86qhFc=akyL;WZ%mQ`-UqgB5=PT0SCZWw)z?kH z52s;2<2CLvFdQWu(*_7&bDsDy4GEc|3Up z#OR!bTwI+7VG$EMGHtz>%D?4E@U2;Br2I)43`R4!16Xnd+GU-G#o{wuTaCU@dG&5> zy~*10jH@?f?`tx5hDiedI>#ljJ<1(OaIfPY;T9bw+;<_W!uXc=Bkv?n#vVKheWQR3 z{wb3mn}m=VHwpRwcSFH;A-@1#C(|G#K9S+Bw%{?&C!)Ha3HhIt7kwhAY5EnHeVfr+ zs~8Gf+6M+z&6esGe_ObshOC%{EkZnX1Zds^oU7#1MZ9~73Q~sPw$FtUw&gMY2ivKW zN%}k#9e}@fpstro<(@#a@IiJu9_r=qpnjUMUkBN?%6-AeD1_TW1D0NA zZ)-kRaY1Ah@E>k|E(TtJ%wpbrSA40a4w^Tl7qU7JUo!H@j-V#FtqCxl0=c#-oVw=RnPY@F`lmF>E8aOyaBW75@Y z9o4O^wl3@7o_)<~vqtUoHj?$ZR1<4fS{;L9&l0+;v$U{CpID!%1M$`?0ttTh{p^gz zJI~V4u=w8JzjlJ-a+$*_=a8b+9UQCgH~ZT{?fs)hqR_2R#w$AjuSR8*gY99t|LK{ zI*;r4C(CR1-eHtb7z?*5EB#-6tZ`gC4bJN zP9(MTIJX79jv3b_vdHX4o?Y4c>ylsGhl>n~u1L4w=}LYc?z|$E;ZLqhD%iOz(p(-c zEYrEX3|v{2&|7(eSXOD`LA|T9xys`mQ2RW+eQIrcQ}ggJs;2l7ZY!{vSEaLn-(98) bQNB#?!tK|jIowYhmOdq=*qhg+XN&#^K{jj) diff --git a/Assets/0.SCENE/MainGame.unity b/Assets/0.SCENE/MainGame.unity index cf7b9a93..927d4c9b 100644 --- a/Assets/0.SCENE/MainGame.unity +++ b/Assets/0.SCENE/MainGame.unity @@ -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 diff --git a/Assets/1.myPrefab/MyMonster/KamikazeMonster.prefab b/Assets/1.myPrefab/MyMonster/KamikazeMonster.prefab index 06512364..37bbcf89 100644 --- a/Assets/1.myPrefab/MyMonster/KamikazeMonster.prefab +++ b/Assets/1.myPrefab/MyMonster/KamikazeMonster.prefab @@ -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 ' diff --git a/Assets/1.myPrefab/MyMonster/Monster Weapon/Throw Monster Weapon.prefab b/Assets/1.myPrefab/MyMonster/Monster Weapon/Throw Monster Weapon.prefab index b19393e1..7c1d2ecb 100644 --- a/Assets/1.myPrefab/MyMonster/Monster Weapon/Throw Monster Weapon.prefab +++ b/Assets/1.myPrefab/MyMonster/Monster Weapon/Throw Monster Weapon.prefab @@ -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: diff --git a/Assets/1.myPrefab/MyMonster/RushMonster2.prefab b/Assets/1.myPrefab/MyMonster/RushMonster2.prefab index 85772360..bacd86e6 100644 --- a/Assets/1.myPrefab/MyMonster/RushMonster2.prefab +++ b/Assets/1.myPrefab/MyMonster/RushMonster2.prefab @@ -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 diff --git a/Assets/1.myPrefab/MyMonster/SwordMonster.prefab b/Assets/1.myPrefab/MyMonster/SwordMonster.prefab index a89795d5..5f5e0690 100644 --- a/Assets/1.myPrefab/MyMonster/SwordMonster.prefab +++ b/Assets/1.myPrefab/MyMonster/SwordMonster.prefab @@ -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: diff --git a/Assets/1.myPrefab/MyMonster/ThrowMonster.prefab b/Assets/1.myPrefab/MyMonster/ThrowMonster.prefab index 2d725065..59e6bfdd 100644 --- a/Assets/1.myPrefab/MyMonster/ThrowMonster.prefab +++ b/Assets/1.myPrefab/MyMonster/ThrowMonster.prefab @@ -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: [] diff --git a/Assets/Epic Toon FX/Prefabs/Combat/Missiles/Fireball/FireballMissileFire 1.prefab b/Assets/Epic Toon FX/Prefabs/Combat/Missiles/Fireball/FireballMissileFire 1.prefab index 69b92406..2438dc36 100644 --- a/Assets/Epic Toon FX/Prefabs/Combat/Missiles/Fireball/FireballMissileFire 1.prefab +++ b/Assets/Epic Toon FX/Prefabs/Combat/Missiles/Fireball/FireballMissileFire 1.prefab @@ -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: diff --git a/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs b/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs index 9dda896c..3fb2038f 100644 --- a/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs +++ b/Assets/Scripts/Core/Utils/StatusEffectProcessor.cs @@ -3,7 +3,8 @@ using UnityEngine.AI; // 내비게이션 기능을 불러올거에요 -> UnityEn using System.Collections; // 코루틴을 사용할거에요 -> System.Collections를 /// -/// 화상, 독, 슬로우 등의 상태이상을 전담 처리하는 클래스 +/// 화상, 독, 슬로우, 감전 등의 상태이상을 전담 처리하는 클래스 +/// [UPGRADED] 틱 간격(tickInterval) 지원 오버로드 + ApplyPoison 추가 /// 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) // 코루틴 함수를 정의할거에요 -> 화상 로직을 + /// + /// [기존] 화상 — 2-파라미터 (하위 호환, 기본 틱 간격 0.5초) + /// + 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) // 코루틴 함수를 정의할거에요 -> 슬로우 로직을 + /// + /// [NEW] 화상 — 3-파라미터 (틱 간격 지정 가능) + /// + public void ApplyBurn(float damagePerTick, float duration, float tickInterval) // 함수를 선언할거에요 -> 화상 적용을 (3-파라미터 버전) + { + _owner.StartCoroutine(DotRoutine("화상", damagePerTick, duration, tickInterval)); // 실행할거에요 -> 지정 틱 간격으로 도트 코루틴을 + } + + // ───────────────────────────────────────────────────────── + // 독 (Poison) + // ───────────────────────────────────────────────────────── + + /// + /// [NEW] 독 — 2-파라미터 (기본 틱 간격 1초) + /// + public void ApplyPoison(float damage, float duration) // 함수를 선언할거에요 -> 독 적용을 (2-파라미터 버전) + { + _owner.StartCoroutine(DotRoutine("독", damage, duration, 1f)); // 실행할거에요 -> 기본 틱 간격 1초로 도트 코루틴을 + } + + /// + /// [NEW] 독 — 3-파라미터 (틱 간격 지정 가능) + /// + public void ApplyPoison(float damagePerTick, float duration, float tickInterval) // 함수를 선언할거에요 -> 독 적용을 (3-파라미터 버전) + { + _owner.StartCoroutine(DotRoutine("독", damagePerTick, duration, tickInterval)); // 실행할거에요 -> 지정 틱 간격으로 도트 코루틴을 + } + + // ───────────────────────────────────────────────────────── + // 슬로우 (Slow) + // ───────────────────────────────────────────────────────── + + /// + /// [기존] 슬로우 — 2-파라미터 + /// amount: 감속 비율 (0~1 사이 값 또는 0~100 사이 값 모두 지원) + /// + public void ApplySlow(float amount, float duration) // 함수를 선언할거에요 -> 슬로우 적용을 + { + _owner.StartCoroutine(SlowRoutine(amount, duration)); // 실행할거에요 -> 슬로우 코루틴을 + } + + // ───────────────────────────────────────────────────────── + // 감전/스턴 (Shock) + // ───────────────────────────────────────────────────────── + + /// + /// [기존] 감전 — 즉발 데미지 + 스턴 + /// + public void ApplyShock(float damage, float duration) // 함수를 선언할거에요 -> 감전 적용을 + { + _damageable.TakeDamage(damage); // 실행할거에요 -> 즉발 데미지를 + _owner.StartCoroutine(StunRoutine(duration)); // 실행할거에요 -> 스턴 코루틴을 + } + + // ───────────────────────────────────────────────────────── + // 코루틴들 + // ───────────────────────────────────────────────────────── + + /// + /// [NEW] 통합 도트 데미지 코루틴 — 화상/독 모두 이것을 사용 + /// damagePerTick: 1틱당 데미지 + /// duration: 총 지속 시간 (초) + /// tickInterval: 틱 간격 (초) + /// + 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} 종료!"); // 로그를 출력할거에요 -> 효과 종료 안내를 + } + + /// + /// [기존] 슬로우 코루틴 + /// amount가 1 이하면 비율로(0.5 = 50% 감속), 1 초과면 퍼센트로(50 = 50% 감속) + /// + 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) // 코루틴 함수를 정의할거에요 -> 스턴 로직을 + /// + /// [기존] 스턴 코루틴 — 이동 정지 + 애니 멈춤 + /// + 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] 스턴 해제!"); // 로그를 출력할거에요 -> 해제 안내를 } } \ No newline at end of file diff --git a/Assets/Scripts/Enemy/AI/MonsterClass.cs b/Assets/Scripts/Enemy/AI/MonsterClass.cs index 9405ffb5..aabdfba7 100644 --- a/Assets/Scripts/Enemy/AI/MonsterClass.cs +++ b/Assets/Scripts/Enemy/AI/MonsterClass.cs @@ -69,7 +69,7 @@ public abstract class MonsterClass : MonoBehaviour, IDamageable // 추상 클래 agent = GetComponent(); // 가져올거에요 -> 에이전트를 audioSource = GetComponent(); // 가져올거에요 -> 오디오 소스를 mobRenderer = GetComponentInChildren(); // 가져올거에요 -> 렌더러를 - // statusProcessor = GetComponent(); // 가져올거에요 -> 상태이상 처리기를 + 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) // 함수를 선언할거에요 -> 상태이상 적용을 + /// + /// [기존] 상태이상 적용 — 3-파라미터 (하위 호환) + /// + public void ApplyStatusEffect(StatusEffectType type, float dmg, float dur) // 함수를 선언할거에요 -> 상태이상 적용을 (3-파라미터 버전) + { + ApplyStatusEffect(type, dmg, dur, 0.5f, 1f); // 실행할거에요 -> 5-파라미터 버전을 기본 틱간격 0.5초, 강도 1로 + } + + /// + /// [NEW] 상태이상 적용 — 5-파라미터 (Arrow 인스펙터 연동) + /// tickInterval: 도트 데미지 틱 간격 (초) + /// strength: 특수 효과 강도 (슬로우 비율, 스턴 시간 등) + /// + 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; // 분기를 종료할거에요 } } diff --git a/Assets/Scripts/Player/Combat/Arrow.cs b/Assets/Scripts/Player/Combat/Arrow.cs index 4f7c9b50..02a45100 100644 --- a/Assets/Scripts/Player/Combat/Arrow.cs +++ b/Assets/Scripts/Player/Combat/Arrow.cs @@ -4,7 +4,7 @@ using System.Collections; // 코루틴 기능을 사용할거에요 -> System.Co /// /// 발사된 화살 발사체 (파티클 프리팹에 부착됨) /// SphereCast 기반 정밀 충돌 감지 (기존 100% 유지) -/// [NEW] 속성 데미지(DoT) 시스템 추가 +/// [UPGRADED] 속성 데미지를 인스펙터에서 속성별로 개별 조정 가능 /// 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 } /// - /// [MODIFIED] 초기화 — 속성 정보 포함 (7개 파라미터) + /// [UPGRADED] 초기화 — 속성 타입만 받고, 세부 수치는 인스펙터 설정값 사용 /// PlayerAttack.OnShootArrow()에서 호출됩니다. /// 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초 뒤에 안전장치로 } + /// + /// [하위 호환성] 기존 7-파라미터 버전도 유지 (외부에서 직접 수치를 넘기는 경우) + /// + 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-파라미터 초기화를 (인스펙터 값 사용) + } + /// /// [하위 호환성] 방향 없는 3-파라미터 버전 /// - 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); // 실행할거에요 -> 신버전 초기화를 기본값으로 + } + + /// + /// [NEW] 속성 타입에 맞는 인스펙터 설정 데이터를 반환 + /// + 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 } /// - /// [MODIFIED] 충돌 처리 — 속성 데미지 적용 추가 + /// [UPGRADED] 충돌 처리 — 인스펙터 설정값 기반 속성 데미지 적용 /// 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); // 파괴할거에요 -> 화살을 } } /// - /// [NEW] 속성 효과 적용 - /// MonsterClass에 ApplyStatusEffect 메서드가 있어야 합니다. + /// [UPGRADED] 속성 효과 적용 — 인스펙터에서 설정한 값을 직접 사용 + /// MonsterClass에 ApplyStatusEffect(StatusEffectType, float dotDmg, float duration, float tickInterval, float strength) + /// 오버로드가 필요합니다. /// 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 } } + /// + /// [PUBLIC] 외부에서 인스펙터 속성 데이터를 읽을 수 있는 접근자 + /// UI 등에서 속성 정보를 표시할 때 사용 + /// + public ElementEffectData GetCurrentElementData() // 함수를 선언할거에요 -> 현재 속성 데이터를 반환하는 접근자를 + { + return GetElementData(currentElement); // 반환할거에요 -> 현재 속성의 설정 데이터를 + } + + /// + /// [PUBLIC] 현재 속성 타입 반환 + /// + public ArrowElementType GetCurrentElement() // 함수를 선언할거에요 -> 현재 속성 타입을 반환하는 접근자를 + { + return currentElement; // 반환할거에요 -> 현재 속성 타입을 + } + /// /// Gizmo 디버그 시각화 — 기존 로직 유지 + 속성 색상 추가 /// @@ -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); // 그림을 그릴거에요 -> 속성 표시용 구체를 } diff --git a/Assets/Scripts/Player/Combat/ElementEffectData.cs b/Assets/Scripts/Player/Combat/ElementEffectData.cs new file mode 100644 index 00000000..8598a5b7 --- /dev/null +++ b/Assets/Scripts/Player/Combat/ElementEffectData.cs @@ -0,0 +1,27 @@ +using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 + +/// +/// 속성별 데미지 설정을 담는 데이터 클래스 +/// 인스펙터에서 속성별로 데미지, 지속시간, 틱 간격을 조정할 수 있습니다. +/// +[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를 +} \ No newline at end of file diff --git a/Assets/Scripts/Player/Combat/ElementEffectData.cs.meta b/Assets/Scripts/Player/Combat/ElementEffectData.cs.meta new file mode 100644 index 00000000..e63cb4f9 --- /dev/null +++ b/Assets/Scripts/Player/Combat/ElementEffectData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35e52ffeb828c4a449da5a4e6638f64c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Player/Stats/Stats.cs b/Assets/Scripts/Player/Stats/Stats.cs index 5eab9201..336af621 100644 --- a/Assets/Scripts/Player/Stats/Stats.cs +++ b/Assets/Scripts/Player/Stats/Stats.cs @@ -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를 diff --git a/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs b/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs index 12adda1d..435e9ec0 100644 --- a/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs +++ b/Assets/Scripts/UI/Enemy/EnemyHP Ui.cs @@ -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를 +/// +/// 몬스터 체력바 UI — MonsterClass.OnHealthChanged 이벤트 연동 +/// 몬스터 프리팹의 MonsterHP_Canvas에 부착 +/// +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 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; // 할당할거에요 -> 최상위 부모 오브젝트를 + } + + if (targetObject != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 + { + _monster = targetObject.GetComponent(); // 가져올거에요 -> MonsterClass를 + if (_monster == null) // 조건이 맞으면 실행할거에요 -> 직접 못 찾았다면 + { + _monster = targetObject.GetComponentInParent(); // 가져올거에요 -> 부모에서 MonsterClass를 + } + } } - // IDamageable 인터페이스 구현 - public void TakeDamage(float amount) // 함수를 선언할거에요 -> 데미지를 입는 TakeDamage를 + private void OnEnable() // 함수를 실행할거에요 -> 활성화 시 OnEnable을 { - if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽었다면 + if (_monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터가 있다면 + { + _monster.OnHealthChanged += OnMonsterHealthChanged; // 등록할거에요 -> 체력 변경 이벤트 리스너를 + } - _currentHealth = Mathf.Max(0, _currentHealth - amount); // 값을 계산할거에요 -> 체력에서 데미지를 빼고 0 밑으로 안 내려가게 - - // ⭐ 피격 시 로그와 함께 UI 갱신 신호 발송 - Debug.Log($"{gameObject.name} 피격! 체력: {{{_currentHealth}/{maxHealth}}}"); // 로그를 출력할거에요 -> 피격 정보와 남은 체력을 - OnHealthChanged?.Invoke(_currentHealth, maxHealth); // 이벤트를 실행할거에요 -> 변경된 체력 정보를 알리기 위해 - - if (_currentHealth <= 0) Die(); // 조건이 맞으면 실행할거에요 -> 체력이 0 이하라면 사망 처리 Die를 + // 활성화 시 즉시 UI 갱신 (풀에서 재사용될 때 대비) + ResetUI(); // 실행할거에요 -> UI 초기화를 } - private void Die() // 함수를 선언할거에요 -> 사망 처리를 하는 Die를 + private void OnDisable() // 함수를 실행할거에요 -> 비활성화 시 OnDisable을 { - if (IsDead) return; // 조건이 맞으면 중단할거에요 -> 이미 죽음 처리 중이라면 - IsDead = true; // 상태를 바꿀거에요 -> 사망 상태를 참으로 + if (_monster != null) // 조건이 맞으면 실행할거에요 -> 몬스터가 있다면 + { + _monster.OnHealthChanged -= OnMonsterHealthChanged; // 해제할거에요 -> 체력 변경 이벤트 리스너를 + } + } - Debug.Log($"{gameObject.name} 사망!"); // 로그를 출력할거에요 -> 사망 메시지를 - Destroy(gameObject, 1.5f); // 파괴할거에요 -> 이 오브젝트를 1.5초 뒤에 + /// + /// MonsterClass.OnHealthChanged(currentHP, maxHP) 이벤트 콜백 + /// + 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}"; // 텍스트를 설정할거에요 -> "현재 / 최대" 형식으로 + } + } + + /// + /// UI를 풀 체력 상태로 초기화 (스폰/재활성화 시) + /// + private void ResetUI() // 함수를 선언할거에요 -> UI 초기화를 + { + if (hpFilledImage != null) // 조건이 맞으면 실행할거에요 -> Fill 이미지가 있다면 + { + hpFilledImage.fillAmount = 1f; // 값을 설정할거에요 -> Fill을 꽉 채우기로 + } + + // _monster가 있으면 실제 스탯으로 텍스트 갱신 + if (hpText != null) // 조건이 맞으면 실행할거에요 -> 텍스트가 있다면 + { + hpText.text = ""; // 텍스트를 비울거에요 -> 초기화로 (첫 OnHealthChanged에서 갱신됨) + } } } \ No newline at end of file diff --git a/Assets/Scripts/UI/HUD/HP Ui bar.cs b/Assets/Scripts/UI/HUD/HP Ui bar.cs index 70553fba..6f84f455 100644 --- a/Assets/Scripts/UI/HUD/HP Ui bar.cs +++ b/Assets/Scripts/UI/HUD/HP Ui bar.cs @@ -1,36 +1,100 @@ using UnityEngine; // 유니티 기능을 불러올거에요 -> UnityEngine을 using UnityEngine.UI; // UI 기능을 불러올거에요 -> UnityEngine.UI를 +using TMPro; // TextMeshPro를 사용할거에요 -> TMPro를 +/// +/// 플레이어 체력바 UI — PlayerHealth 이벤트 연동 +/// HP_Filled (Image Fill) + Text (TMP) 실시간 갱신 +/// 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(); // 가져올거에요 -> 슬라이더를 if (targetObject != null) // 조건이 맞으면 실행할거에요 -> 타겟이 있다면 { - _damageable = targetObject.GetComponent(); // 가져올거에요 -> 인터페이스를 - _dummy = targetObject.GetComponent(); // 가져올거에요 -> 더미 봇을 + _playerHealth = targetObject.GetComponent(); // 가져올거에요 -> PlayerHealth를 + _playerStats = targetObject.GetComponent(); // 가져올거에요 -> 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 갱신을 (시작하자마자 올바른 값 표시) + } + + /// + /// PlayerHealth.RefreshHealthUI()에서 ratio(0~1)를 받아 호출됨 + /// + private void OnHealthRatioChanged(float ratio) // 함수를 선언할거에요 -> 체력 비율 변경 콜백을 + { + // Fill Amount 갱신 + if (hpFilledImage != null) // 조건이 맞으면 실행할거에요 -> Fill 이미지가 있다면 + { + hpFilledImage.fillAmount = Mathf.Clamp01(ratio); // 값을 설정할거에요 -> 0~1 사이로 Fill Amount를 + } + + // 텍스트 갱신 + UpdateHPText(); // 실행할거에요 -> 텍스트 갱신 함수를 + } + + /// + /// 텍스트를 "현재HP / 최대HP" 형식으로 갱신 + /// + 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}"; // 텍스트를 설정할거에요 -> "현재 / 최대" 형식으로 + } + + /// + /// 이벤트 없이 직접 UI를 갱신 (Start, 수동 호출용) + /// + 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(); // 실행할거에요 -> 텍스트 갱신을 + } } \ No newline at end of file