반응형
오늘의 목표
- 타워 특성 추가
- 치명타 - 기본 대미지를 특정 배율로 변경하여 공격
- 슬로우 - 적의 이동속도를 특정 시간동안 늦춤
- 체인 - 특정 타겟 수에게 전이되며 줄어드는 대미지를 입히는 공격
- 폭발 - 특정 범위에 대미지를 입히는 공격
- 대미지 시각화
- 몬스터의 상단에 대미지를 출력
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
using System.Collections.Generic;
using UnityEngine;
public class TowerTraitDatabase : MonoBehaviour
{
[Header("All Traits")]
[SerializeField] private TowerTraitSO[] allTraits;
[System.Serializable]
public struct AffinityWeights
{
public int core;
public int common;
public int wild;
public AffinityWeights(int core, int common, int wild)
{
this.core = core;
this.common = common;
this.wild = wild;
}
}
// 타워 타입별 성향 가중치
private static readonly Dictionary<string, AffinityWeights> AffinityWeightByType =
new Dictionary<string, AffinityWeights>
{
{ "basic", new AffinityWeights(50, 45, 5) },
{ "cannon", new AffinityWeights(60, 30, 10) },
{ "magic", new AffinityWeights(45, 35, 20) },
};
public TowerTraitSO RollTrait(string towerId, TowerGrade grade)
{
if (allTraits == null || allTraits.Length == 0)
return null;
// 1) 타워 타입 키(basic/cannon/magic)
string typeKey = GetTypeKeyFromTowerId(towerId);
// 2) 등급별 티어 확률로 티어 결정
TraitTier tier = RollTierByGrade(grade);
// 3) 타입별 성향 가중치로 affinity 결정
AffinityWeights w = AffinityWeightByType.TryGetValue(typeKey, out var found)
? found
: new AffinityWeights(50, 40, 10);
TraitAffinity affinity = RollAffinity(w);
// 4) 후보 수집: (tier + affinity) 매칭
List<TowerTraitSO> candidates = new List<TowerTraitSO>(16);
for (int i = 0; i < allTraits.Length; i++)
{
var t = allTraits[i];
if (t == null) continue;
if (t.tier != tier) continue;
if (t.affinity != affinity) continue;
candidates.Add(t);
}
// 5) 폴백: tier만 맞는 걸로라도 뽑기 (데이터 미구성 대비)
if (candidates.Count == 0)
{
for (int i = 0; i < allTraits.Length; i++)
{
var t = allTraits[i];
if (t == null) continue;
if (t.tier == tier)
candidates.Add(t);
}
}
if (candidates.Count == 0)
return null;
return candidates[Random.Range(0, candidates.Count)];
}
private static TraitTier RollTierByGrade(TowerGrade grade)
{
// 네 룰: Normal은 Trait 없음이니 이 함수는 Rare/Epic/Legend에만 호출한다고 가정
int t1, t2, t3;
switch (grade)
{
case TowerGrade.Rare:
t1 = 80; t2 = 20; t3 = 0;
break;
case TowerGrade.Epic:
t1 = 50; t2 = 45; t3 = 5;
break;
case TowerGrade.Legendary:
t1 = 25; t2 = 55; t3 = 20;
break;
default:
t1 = 100; t2 = 0; t3 = 0;
break;
}
int sum = t1 + t2 + t3;
int roll = Random.Range(1, sum + 1);
if (roll <= t1) return TraitTier.T1;
roll -= t1;
if (roll <= t2) return TraitTier.T2;
return TraitTier.T3;
}
private static TraitAffinity RollAffinity(AffinityWeights w)
{
int core = Mathf.Max(0, w.core);
int common = Mathf.Max(0, w.common);
int wild = Mathf.Max(0, w.wild);
int sum = Mathf.Max(1, core + common + wild);
int roll = Random.Range(1, sum + 1);
if (roll <= core) return TraitAffinity.Core;
roll -= core;
if (roll <= common) return TraitAffinity.Common;
return TraitAffinity.Wild;
}
private static string GetTypeKeyFromTowerId(string towerId)
{
if (string.IsNullOrEmpty(towerId))
return "basic";
string lower = towerId.ToLower();
if (lower.Contains("cannon")) return "cannon";
if (lower.Contains("magic")) return "magic";
return "basic";
}
}
|
cs |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
using System.Collections.Generic;
using UnityEngine;
public static class TraitProcessor
{
public static LayerMask MonsterLayerMask = ~0;
public static int ModifyDamage(TowerTraitSO trait, int baseDamage)
{
if (trait == null) return baseDamage;
switch (trait.type)
{
case TowerTraitType.Critical:
{
// value=확률(0~1), range=배율(>=1)
float chance = Mathf.Clamp01(trait.value);
float mul = Mathf.Max(1f, trait.range);
if (Random.value < chance)
return Mathf.RoundToInt(baseDamage * mul);
return baseDamage;
}
default:
return baseDamage;
}
}
public static void ApplyOnHit(TowerTraitSO trait, TowerBase source, MonsterAI target, int hitDamage)
{
if (trait == null || target == null) return;
switch (trait.type)
{
case TowerTraitType.Slow:
ApplySlow(trait, target);
break;
case TowerTraitType.Chain:
ApplyChain(trait, target, hitDamage);
break;
case TowerTraitType.Explosion:
ApplyExplosion(trait, target.transform.position, target, hitDamage);
break;
}
}
private static void ApplySlow(TowerTraitSO trait, MonsterAI target)
{
// value=둔화율(0.3이면 30% 느려짐), duration=지속시간
float slowRate = Mathf.Clamp01(trait.value);
float dur = Mathf.Max(0.1f, trait.duration);
target.ApplySlow(slowRate, dur);
}
private static void ApplyChain(TowerTraitSO trait, MonsterAI firstTarget, int hitDamage)
{
// range = 다음 타겟 탐색 반경
// count = 추가로 맞출 타겟 수
// value = 연쇄 데미지 비율(원 데미지 대비)
float searchRadius = Mathf.Max(0.1f, trait.range);
int jumps = Mathf.Max(0, trait.count);
float dmgRatio = Mathf.Clamp01(trait.value);
if (jumps <= 0 || dmgRatio <= 0f) return;
int chainDamage = Mathf.Max(1, Mathf.RoundToInt(hitDamage * dmgRatio));
// 이미 맞은 대상은 다시 맞지 않도록
HashSet<MonsterAI> hitSet = new HashSet<MonsterAI>();
hitSet.Add(firstTarget);
MonsterAI current = firstTarget;
for (int i = 0; i < jumps; i++)
{
MonsterAI next = FindNearestMonster(current.transform.position, searchRadius, hitSet);
if (next == null) break;
hitSet.Add(next);
next.TakeDamage(chainDamage);
current = next;
}
}
private static void ApplyExplosion(TowerTraitSO trait, Vector3 center, MonsterAI directTarget, int hitDamage)
{
// range = 폭발 반경
// value = 폭발 데미지 비율
float radius = Mathf.Max(0.1f, trait.range);
float dmgRatio = Mathf.Clamp01(trait.value);
if (dmgRatio <= 0f) return;
int splashDamage = Mathf.Max(1, Mathf.RoundToInt(hitDamage * dmgRatio));
Collider[] cols = Physics.OverlapSphere(center, radius, MonsterLayerMask);
for (int i = 0; i < cols.Length; i++)
{
MonsterAI m = cols[i].GetComponentInParent<MonsterAI>();
if (m == null) continue;
if (m == directTarget) continue;
m.TakeDamage(splashDamage);
}
}
private static MonsterAI FindNearestMonster(Vector3 center, float radius, HashSet<MonsterAI> exclude)
{
Collider[] cols = Physics.OverlapSphere(center, radius, MonsterLayerMask);
MonsterAI best = null;
float bestDistSq = float.PositiveInfinity;
for (int i = 0; i < cols.Length; i++)
{
MonsterAI m = cols[i].GetComponentInParent<MonsterAI>();
if (m == null) continue;
if (exclude != null && exclude.Contains(m)) continue;
float distSq = (m.transform.position - center).sqrMagnitude;
if (distSq < bestDistSq)
{
bestDistSq = distSq;
best = m;
}
}
return best;
}
}
|
cs |
추가 된 내용을 타워 매니저에서 사용 할 수 있도록 추가하고 적용합니다.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
private void AssignTraitIfNeeded(TowerBase tower)
{
if (tower == null) return;
TowerData d = tower.GetData();
if (d == null) return;
if (d.grade == TowerGrade.Normal)
{
tower.SetTrait(null);
return;
}
if (traitDatabase == null)
{
Debug.LogWarning("[TowerManager] traitDatabase is null.");
tower.SetTrait(null);
return;
}
TowerTraitSO rolled = traitDatabase.RollTrait(d.towerId, d.grade);
tower.SetTrait(rolled);
if (rolled != null)
Debug.Log($"[Trait] {d.towerId}({d.grade}) => {rolled.type} {rolled.tier}");
}
|
cs |
반응형