Files
DS-Combat_analy/ARCHITECTURE.md
2025-10-28 12:34:12 +09:00

1469 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ARCHITECTURE.md
던전 스토커즈 전투 분석 시스템 - 기술 아키텍처 문서
- **목적**: 데이터 구조, 추출 로직, 판정 알고리즘 등 구현 세부사항 문서화
- **대상**: 개발자, 분석 스크립트 유지보수자
- **최종 업데이트**: 2025-10-27
---
## 📁 1. 데이터 소스 구조
### 1.1 JSON 파일 형식
모든 JSON 파일은 동일한 최상위 구조를 가집니다:
```json
{
"ExportedAt": "2025-10-24T15:58:55",
"TotalCount": 107,
"Assets": [
{
"AssetName": "DT_Skill",
"AssetPath": "/Game/Blueprints/DataTable/DT_Skill.DT_Skill",
"RowStructure": "SkillDataRow",
"Rows": [...]
}
]
}
```
**중요**: `Assets`는 배열이며, 각 요소는 `AssetName`으로 식별됩니다.
---
## 📊 2. DataTable.json 구조
### 2.1 DT_Skill (스킬 정의 테이블)
**위치**: `Assets` → AssetName == "DT_Skill"
**Row 구조**: `Rows` 배열 → 각 Row는 `{ "RowName": "SK110101", "Data": {...} }`
#### Data 필드 (주요)
| 필드 | 타입 | 설명 | 예시 |
|------|------|------|------|
| `name` | string | 스킬 이름 (한글) | "독성 화살" |
| `stalkerName` | string | 소속 스토커 | "urud" |
| `skillDamageRate` | float | 피해 배율 | 1.2 |
| `coolTime` | float | 쿨타임 (초) | 7.5 |
| `manaCost` | int | 마나 소모량 | 12 |
| `castingTime` | float | 시전 시간 (초) | 2.0 |
| `useMontages` | array | 몽타주 경로 배열 | `["/Script/Engine.AnimMontage'/Game/.../AM_Urud_Shot.AM_Urud_Shot'"]` |
| `desc` | string | 설명 (템플릿) | "피해 {0}% 증가" |
| `descValues` | array | 설명 치환 값 | `[3.8, 6.8]` |
| `bIsUltimate` | bool | 궁극기 여부 | true/false |
| `skillAttackType` | string | 공격 타입 | "PhysicalSkill", "MagicSkill" |
#### 몽타주 경로 추출
```python
# useMontages에서 몽타주 이름 추출
montage_path = row_data.get('useMontages', [])[0]
# 예: "/Script/Engine.AnimMontage'/Game/.../AM_Urud_Shot.AM_Urud_Shot'"
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
# 결과: "AM_Urud_Shot"
```
#### ⚠️ 주의사항
1. **descValues 소수점 오류**: JSON 추출 과정에서 `3.799999952316284` 같은 값 발생
- **해결**: 모든 float 값을 `round(val, 2)`로 소수점 둘째자리 반올림
2. **useMontages는 배열**: 대부분 1개 요소, 일부 스킬은 2개 (예: SK150201)
- 첫 번째 몽타주가 시전 준비, 두 번째가 실제 공격
### 2.2 DT_CharacterAbility (캐릭터 기본 능력)
**위치**: `Assets` → AssetName == "DT_CharacterAbility"
**Row 구조**: `Rows` 배열 → 각 Row는 `{ "RowName": "urud", "Data": {...} }`
#### Data 필드 (주요)
| 필드 | 타입 | 설명 |
|------|------|------|
| `abilities` | array | 보유 스킬 목록 |
| `effects` | array | 패시브 효과 |
| `tags` | object | 게임플레이 태그 |
| `montageMap` | dict | 스킬 몽타주 맵 |
| `attackMontageMap` | dict | **평타 몽타주 맵** ⭐ |
#### attackMontageMap 구조 (평타 추출)
```json
{
"attackMontageMap": {
"bow": {
"abilityClass": "None",
"montageArray": [
"/Script/Engine.AnimMontage'/Game/_Art/_Character/PC/Urud/AnimMontage/Base/AM_PC_Urud_Base_B_Attack_N.AM_PC_Urud_Base_B_Attack_N'"
]
}
}
}
```
**추출 방법**:
```python
attack_map = row_data.get('attackMontageMap', {})
# 무기 타입별로 평타 몽타주가 다를 수 있음
for weapon_type, weapon_data in attack_map.items():
montage_array = weapon_data.get('montageArray', [])
if montage_array:
basic_attack_montage = montage_array[0]
```
**모든 스토커 공통 규칙**:
- 평타는 `DT_CharacterAbility.attackMontageMap`에 정의됨
- 스킬은 `DT_Skill.useMontages`에 정의됨
### 2.3 DT_CharacterStat (캐릭터 스탯)
**위치**: `Assets` → AssetName == "DT_CharacterStat"
#### Data 필드
| 필드 | 타입 | 설명 |
|------|------|------|
| `strength` | int | 힘 |
| `dexterity` | int | 민첩 |
| `intelligence` | int | 지능 |
| `constitution` | int | 체력 |
| `wisdom` | int | 지혜 |
| `maxHP` | int | 최대 HP |
| `maxMP` | int | 최대 MP |
| `manaRegen` | float | 마나 회복/초 |
---
## 🎬 3. AnimMontage.json 구조
### 3.1 AnimMontage Asset 구조
**위치**: `Assets` → Type은 없고 AssetName으로 식별
**Asset 구조**: `{ "AssetName": "AM_Urud_Shot", "AnimNotifies": [...], ... }`
#### 주요 필드
| 필드 | 타입 | 설명 |
|------|------|------|
| `AssetName` | string | 몽타주 이름 |
| `AssetPath` | string | 전체 경로 |
| `AnimNotifies` | array | **애니메이션 노티파이 배열** ⭐ |
| `AnimNotifyStates` | array | 노티파이 스테이트 배열 |
| `SequenceLength` | float | 시퀀스 전체 길이 |
### 3.2 AnimNotifies 구조 (공격 판정 핵심)
각 노티파이는 다음 구조를 가집니다:
```json
{
"NotifyClass": "AN_WSAttack_GAS_C",
"TriggerTime": 0.85,
"CustomProperties": {
"AttackMultiplier": "3.5",
"Event Tag": "(TagName=\"Event.SkillActivate\")"
}
}
```
#### 3.2.1 주요 NotifyClass 타입
| NotifyClass | 용도 | 판정 기준 |
|-------------|------|-----------|
| `AN_WSAttack_GAS_C` | GAS 기반 공격 | **공격 스킬** ✓ |
| `AN_WSAttack_Set_C` | Set 방식 공격 | **공격 스킬** ✓ |
| `AN_Trigger_Projectile_Shot_C` | 투사체 발사 | **공격 스킬** ✓ |
| `AN_SimpleSendEvent_C` | 이벤트 전송 | CustomProperties 확인 필요 |
| `ANS_SkillCancel_C` | 스킬 캔슬 윈도우 | 유틸리티 |
| `AN_ShowFirearmProjectile_C` | 투사체 표시 | 시각 효과 (공격 아님) |
#### 3.2.2 AN_SimpleSendEvent_C CustomProperties 판정
**CustomProperties 구조**:
```json
{
"Event Tag": "(TagName=\"Event.SkillActivate\")",
"NotifyColor": "(B=200,G=200,R=255,A=255)",
"bShouldFireInEditor": ""
}
```
**Event Tag 파싱**:
```python
custom_props = notify.get('CustomProperties', {})
event_tag = custom_props.get('Event Tag', '')
# Event Tag 형식: (TagName="Event.SkillActivate")
# 추출: "Event.SkillActivate"
if 'SkillActivate' in event_tag:
# 공격 스킬
elif 'SpawnProjectile' in event_tag:
# 공격 스킬 (투사체 생성)
elif 'AttackFire' in event_tag:
# 공격 스킬 (발사)
```
**주요 공격 Event Tag**:
- `Event.SkillActivate` - 스킬 활성화 (바란 일격분쇄, 클라드 다시 흙으로)
- `Event.SpawnProjectile` - 투사체 생성 (리옌 연화)
- `Event.AttackFire` - 공격 발사
**비공격 Event Tag**:
- `Event.BlockingStart` - 방어 시작
- `Ability.Attack.Ready` - 공격 준비 (공격 아님)
#### 3.2.3 TriggerTime (애니메이션 이벤트 시점)
`TriggerTime`은 몽타주 시작부터 노티파이 발동까지의 시간(초)입니다.
**actualDuration (시퀀스 길이) 계산**:
```python
# SequenceLength와 RateScale을 사용하여 계산
sequence_length = montage.get('SequenceLength', 0)
rate_scale = montage.get('RateScale', 1.0)
actual_duration = sequence_length / rate_scale if rate_scale > 0 else sequence_length
```
**중요**:
- **모든 스킬/평타**: 시퀀스 길이(actualDuration)를 사용하여 통일
- **DPS 계산**: `skillDamageRate / (actualDuration + castingTime)`
---
## 🎯 4. 공격 스킬 판정 로직 (우선순위)
### 4.1 판정 기준
**핵심 원칙**: 실질적으로 데미지가 발생하는 시점을 나타내는 노티파이의 존재 여부로 판정
### 4.2 판정 알고리즘
```python
def is_attack_skill(montage_data):
"""
공격 스킬 여부를 판정합니다.
우선순위:
1. AnimNotify의 NotifyName에 공격 키워드 포함 (부분 매칭)
2. AN_SimpleSendEvent 노티파이의 Event Tag 확인
"""
for montage in montage_data:
for notify in montage.get('AnimNotifies', []):
notify_name = notify.get('NotifyName', '')
notify_class = notify.get('NotifyClass', '')
# 1. NotifyName에 키워드 포함 (부분 매칭)
attack_keywords = ['AttackWithEquip', 'Projectile', 'SkillActive']
if any(keyword in notify_name for keyword in attack_keywords):
return True # 공격 스킬
# 2. SimpleSendEvent의 Event Tag 확인 (1순위에 해당되지 않을 때)
if 'SimpleSendEvent' in notify_class:
custom_props = notify.get('CustomProperties', {})
event_tag = custom_props.get('Event Tag', '')
# 공격 Event Tag
if 'Event.SkillActivate' in event_tag:
return True # 스킬 활성화 (바란, 클라드 등)
if 'Event.SpawnProjectile' in event_tag:
return True # 투사체 생성 (리옌 연화 등)
# 공격 노티파이가 없으면 공격 스킬 아님
return False
```
### 4.3 NotifyName 키워드 상세
| 키워드 | 설명 | 예시 |
|--------|------|------|
| **AttackWithEquip** | 무기 공격 (근접) | AttackWithEquip |
| **Projectile** | 투사체 발사 | AN_Projectile_C, AN_Trigger_Projectile_Shot_C, AN_ShowFirearmProjectile_C |
| **SkillActive** | 스킬 활성화 | AN_Trigger_Skill_Active_C |
**부분 매칭 예시**:
- `NotifyName == "AN_Trigger_Projectile_Shot_C"` → "Projectile" 포함 → ✅ 공격
- `NotifyName == "AN_ShowFirearmProjectile_C"` → "Projectile" 포함 → ✅ 공격
- `NotifyName == "AN_Trigger_Skill_Active_C"` → "SkillActive" 포함 → ✅ 공격
### 4.2 예외 케이스
#### 4.2.1 재장전 스킬 (유틸리티)
**스킬 ID**: SK110207 (우르드), SK190209 (리옌)
**특징**:
- AssetName에 "attack" 또는 "reload" 키워드 있음
- **하지만 공격 노티파이 없음** → 유틸리티로 판정
```python
# 재장전 스킬 예외 처리
if 'Reload' in asset_name or skill_id in ['SK110207', 'SK190209']:
# 노티파이 확인 필요
if not has_attack_notify(montage):
return False # 유틸리티
```
#### 4.2.2 차징 스킬 (공격)
**스킬 ID**: SK190101 (리옌 정조준)
**특징**:
- 차징 중에는 공격하지 않음
- **하지만 `AN_Trigger_Projectile_Shot_C` 노티파이 있음** → 공격 스킬
```python
# 차징 후 발사하는 스킬도 공격 스킬
if 'Charging' in asset_name:
if has_projectile_notify(montage):
return True # 공격 스킬
```
#### 4.2.3 소환 스킬 (공격)
**스킬 ID**: SK160202 (Rene Ifrit), SK160206 (Rene Shiva)
**특징**:
- 스킬 자체는 소환 동작
- **소환된 정령이 공격함** → 소환체 데이터 별도 처리
**처리 방법**:
1. 소환 스킬 자체는 skillDamageRate에 따라 공격/유틸리티 판정
2. 소환체 데이터는 Blueprint.json에서 추출
3. **문서에서는 "소환체" 섹션 분리**
---
## 🔧 5. 특수 데이터 처리
### 5.1 DoT (Damage over Time) 스킬
**정의 위치**: `config.py`
```python
DOT_SKILLS = {
'SK110204': {'dot_type': 'Poison', 'stalker': 'urud'}, # 독성 화살
'SK160203': {'dot_type': 'Bleed', 'stalker': 'rene'}, # 독기 화살
'SK170201': {'dot_type': 'Burn', 'stalker': 'cazimord'}, # 작열
'SK160202': {'dot_type': 'Burn', 'stalker': 'rene'}, # Ifrit
}
DOT_DAMAGE = {
'Poison': {
'rate': 0.20, # 대상 MaxHP의 20%
'duration': 5, # 5초간
'description': '대상 MaxHP의 20% (5초간)'
},
'Burn': {
'rate': 0.10, # 대상 MaxHP의 10%
'duration': 3, # 3초간
'description': '대상 MaxHP의 10% (3초간)'
},
'Bleed': {
'damage': 20, # 고정 20 피해
'duration': 5, # 5초간
'description': '고정 20 피해 (5초간)'
}
}
```
**DPS 계산 시 고려**:
- 기본 DPS = `skillDamageRate / (actualDuration + castingTime)`
- DoT DPS = `DoT 피해량 / DoT 지속시간`
- 총 DPS = 기본 DPS + DoT DPS (대상 HP에 따라 변동)
### 5.2 소환체 (Summons)
**소환체가 있는 스토커**: Rene (레네) 만
**소환 스킬**:
- SK160202: 정령 소환 : 화염 (Ifrit)
- SK160206: 정령 소환 : 냉기 (Shiva)
**데이터 구조**:
```json
{
"summonClass": "/Game/Blueprints/Characters/Rene/BP_Ifrit.BP_Ifrit_C",
"skillDamageRate": 1.2, // 소환체가 이 배율로 공격
"duration": 30 // 소환 지속 시간
}
```
**문서 표시 방법**:
```markdown
### SK160202 정령 소환 : 화염
- **스킬 타입**: 소환
- **피해 배율**: 1.2 (정령이 대행)
- **마나**: 15
- **쿨타임**: 20초
## 소환체
### 🔥 화염 정령 (Ifrit)
- **소환 스킬**: SK160202 정령 소환 : 화염
- **공격력**: 1.2 (소환자 공격력 대행)
- **공격 속도**: [Blueprint에서 추출]
- **지속시간**: 30초
- **특수 효과**: Burn DoT (MaxHP 10%, 3초)
```
---
## 📐 6. DPS 계산 공식
### 6.1 기본 DPS
```python
# 평타 DPS
basic_dps = attack_damage_rate / actual_duration
# 스킬 DPS
skill_dps = skill_damage_rate / (actual_duration + casting_time)
```
### 6.2 actualDuration (시퀀스 길이) 계산
```python
def calculate_actual_duration(montage_data):
"""
시퀀스 길이를 계산합니다.
모든 스킬과 평타에 대해 통일된 방식으로 계산합니다.
"""
sequence_length = montage_data.get('SequenceLength', 0)
rate_scale = montage_data.get('RateScale', 1.0)
if rate_scale > 0:
actual_duration = sequence_length / rate_scale
else:
actual_duration = sequence_length
return actual_duration
```
### 6.3 DoT DPS
```python
def calculate_dot_dps(skill_id, target_max_hp):
"""
DoT DPS를 계산합니다. 대상 HP에 따라 변동됩니다.
"""
if skill_id not in DOT_SKILLS:
return 0
dot_info = DOT_SKILLS[skill_id]
dot_type = dot_info['dot_type']
dot_config = DOT_DAMAGE[dot_type]
if 'rate' in dot_config:
# 비율 기반 (Poison, Burn)
total_damage = target_max_hp * dot_config['rate']
else:
# 고정 피해 (Bleed)
total_damage = dot_config['damage']
duration = dot_config['duration']
return total_damage / duration
```
---
## 🛠️ 7. 구현 노하우
### 7.1 JSON 파싱 주의사항
#### 7.1.1 최상위 구조 파악
```python
# ❌ 잘못된 접근
for item in data: # data가 dict이면 에러
...
# ✅ 올바른 접근
assets = data.get('Assets', [])
for asset in assets:
...
```
#### 7.1.2 Asset 찾기
```python
# AssetName으로 찾기
dt_skill = None
for asset in data.get('Assets', []):
if asset.get('AssetName') == 'DT_Skill':
dt_skill = asset
break
# Rows 접근
rows = dt_skill.get('Rows', [])
for row in rows:
row_name = row.get('RowName') # 예: "SK110101"
row_data = row.get('Data', {}) # 실제 데이터
```
### 7.2 몽타주 경로 파싱
```python
# 전체 경로
path = "/Script/Engine.AnimMontage'/Game/_Art/_Character/PC/Urud/AnimMontage/AM_Urud_Shot.AM_Urud_Shot'"
# 몽타주 이름 추출
montage_name = path.split('/')[-1] # "AM_Urud_Shot.AM_Urud_Shot'"
montage_name = montage_name.replace("'", "") # "AM_Urud_Shot.AM_Urud_Shot"
montage_name = montage_name.split('.')[0] # "AM_Urud_Shot"
```
### 7.3 CustomProperties 파싱
```python
# Event Tag 추출
custom_props = notify.get('CustomProperties', {})
event_tag = custom_props.get('Event Tag', '')
# 형식: (TagName="Event.SkillActivate")
# 단순 포함 검사로 충분
if 'SkillActivate' in event_tag:
is_attack = True
```
### 7.4 소수점 반올림
```python
# DT_Skill의 descValues 처리
desc_values_raw = data.get('descValues', [])
desc_values = []
for val in desc_values_raw:
if isinstance(val, float):
desc_values.append(round(val, 2)) # 소수점 둘째자리
else:
desc_values.append(val)
```
### 7.5 디버깅 팁
```python
# 1. Asset 개수 확인
print(f"총 Assets: {len(data.get('Assets', []))}")
# 2. AssetName 목록 출력
for asset in data.get('Assets', []):
print(f" - {asset.get('AssetName')}")
# 3. 노티파이 타입 확인
notify_types = set()
for notify in montage.get('AnimNotifies', []):
notify_types.add(notify.get('NotifyClass', ''))
print(f"노티파이 타입: {notify_types}")
# 4. CustomProperties 전체 출력
if 'CustomProperties' in notify:
print(f"CustomProperties: {notify['CustomProperties']}")
```
---
## 📋 8. 검증 체크리스트
### 8.1 데이터 추출 검증
- [ ] 10명 스토커 모두 추출됨
- [ ] 각 스토커당 평균 5~6개 스킬
- [ ] 궁극기 보유 스토커 확인 (Nave, Baran, Sinobu, Cazimord, Urud, Rio, Rene, Clad, Hilda, Lian)
- [ ] 평타 몽타주 추출 (attackMontageMap)
- [ ] DoT 스킬 4개 확인 (SK110204, SK160203, SK170201, SK160202)
### 8.2 공격 스킬 판정 검증
**수동 확인 필요 (자주 오류 발생)**:
- [ ] SK130301 (바란 일격분쇄) → 공격 스킬
- [ ] SK150201 (클라드 다시 흙으로) → 공격 스킬
- [ ] SK190201 (리옌 연화) → 공격 스킬
- [ ] SK190101 (리옌 정조준) → 공격 스킬
- [ ] SK110207 (우르드 Reload) → 유틸리티
- [ ] SK190209 (리옌 재장전) → 유틸리티
### 8.3 DPS 계산 검증
- [ ] 평타 actualDuration이 0이 아님
- [ ] 모든 스킬의 actualDuration = SequenceLength / RateScale
- [ ] castingTime이 있는 스킬 25개 확인
- [ ] DoT 스킬의 DoT 피해량 표시
### 8.4 문서 품질 검증
- [ ] 모든 스킬에 설명 있음 (descFormatted)
- [ ] descValues 소수점 2자리 (3.8, 6.8)
- [ ] 소환체 섹션 분리 (레네)
- [ ] DoT 종합 비교 테이블
- [ ] 실제 공격 시점 표시 (투사체 스킬)
---
## 🔄 9. 일반적인 오류 및 해결
### 9.1 "공격 스킬을 유틸리티로 잘못 분류"
**원인**:
- SimpleSendEvent의 Event Tag 미확인
- Projectile 노티파이만 있고 Attack 노티파이 없음
**해결**:
1. SimpleSendEvent의 CustomProperties 확인
2. Event.SkillActivate, Event.SpawnProjectile 체크
3. Projectile 노티파이도 공격 판정에 포함
### 9.2 "평타 actualDuration이 0"
**원인**:
- DT_Skill이 아닌 DT_CharacterAbility에서 평타 찾아야 함
- attackMontageMap 파싱 실패
- SequenceLength 또는 RateScale 데이터 누락
**해결**:
```python
# DT_CharacterAbility.attackMontageMap에서 추출
attack_map = char_ability_data.get('attackMontageMap', {})
for weapon_type, weapon_data in attack_map.items():
montage_array = weapon_data.get('montageArray', [])
# actualDuration 계산 확인
sequence_length = montage.get('SequenceLength', 0)
rate_scale = montage.get('RateScale', 1.0)
actual_duration = sequence_length / rate_scale if rate_scale > 0 else sequence_length
```
### 9.3 "DoT 피해가 DPS에 반영 안됨"
**원인**:
- DoT 스킬을 일반 공격 스킬로만 처리
- DoT 별도 계산 로직 누락
**해결**:
1. config.py에 DoT 스킬 정의
2. isDot 플래그 추가
3. DoT 종합 비교 테이블 생성
4. 개별 스킬에 DoT 상세 정보 표시
### 9.4 "descValues가 너무 긴 소수점"
**원인**:
- JSON 추출 시 float 정밀도 문제
**해결**:
```python
if isinstance(val, float):
val = round(val, 2)
```
---
## 📚 10. 참고 데이터
### 10.1 스토커 내부 이름
| 표시 이름 | 내부 이름 | 영문 이름 |
|-----------|-----------|-----------|
| 힐다 | hilda | Hilda |
| 우르드 | urud | Urud |
| 네이브 | nave | Nave |
| 바란 | baran | Baran |
| 리오 | rio | Rio |
| 클라드 | clad | Clad |
| 레네 | rene | Rene |
| 시노부 | sinobu | Sinobu |
| 리옌 | lian | Lian |
| 카지모르드 | cazimord | Cazimord |
### 10.2 스킬 ID 규칙
**형식**: `SK[스토커번호][스킬타입][순번]`
- **스토커 번호**: 11=Hilda, 12=Nave, 13=Baran, 14=Rio, 15=Clad, 16=Rene, 17=Cazimord, 18=Sinobu, 19=Lian, 11=Urud
- **스킬 타입**: 01=기본스킬, 02=서브스킬, 03=궁극기
- **순번**: 01부터 시작
**예시**:
- SK110101: Hilda 기본스킬 1
- SK120202: Nave 서브스킬 2
- SK130301: Baran 궁극기 1
### 10.3 몽타주 명명 규칙
**형식**: `AM_PC_[스토커명]_[카테고리]_[설명]`
**예시**:
- `AM_PC_Urud_Base_B_Attack_N`: 우르드 기본 평타
- `AM_PC_Lian_Base_000_Skill_ChargingBow`: 리옌 차징 스킬
- `AM_PC_Rene_B_Skill_Ifrit_Summon`: 레네 이프리트 소환
---
## 🎓 11. 추가 학습 자료
### 11.1 언리얼 엔진 시스템
- [Gameplay Ability System](https://docs.unrealengine.com/5.5/en-US/gameplay-ability-system-for-unreal-engine/)
- [Animation Notify System](https://docs.unrealengine.com/5.5/en-US/animation-notifies-in-unreal-engine/)
- [DataTable](https://docs.unrealengine.com/5.5/en-US/data-driven-gameplay-elements-in-unreal-engine/)
### 11.2 프로젝트 문서
- [README.md](README.md) - 프로젝트 개요 및 사용 가이드
- [../CLAUDE.md](../CLAUDE.md) - 전체 프로젝트 정보
- [분석도구/v2/장기과제_Blueprint변수검증.md](분석도구/v2/장기과제_Blueprint변수검증.md) - Blueprint 변수 검증 계획
---
## 📊 12. v2 분석 프로세스 (4단계 파이프라인)
### 12.1 프로세스 개요
v2 분석 시스템은 JSON 원본 데이터에서 최종 밸런스 리포트까지 4단계 파이프라인으로 구성됩니다.
```
[원본 JSON] → [01단계] → [02단계] → [03단계] → [04단계]
기본데이터 DPS계산 역할비교 밸런스티어
```
**출력 구조**:
```
분석결과/YYYYMMDD_HHMMSS_v2/
├── 01_스토커별_기본데이터_v2.md # 01단계 출력
├── 02_DPS_시나리오_비교분석_v2.md # 02단계 출력
├── 03_역할별_차별화_v2.md # 03단계 출력
├── 04_밸런스_티어_및_개선안_v2.md # 04단계 출력
├── intermediate_data.json # 중간 데이터
├── validated_data.json # 검증된 데이터
└── 검증_리포트.md # 검증 리포트
```
### 12.2 01단계: 스토커별 기본 데이터
#### 목적
- JSON 원본에서 10명 스토커의 기본 정보 추출 및 검증
- 평타, 스킬, 소환체 데이터 문서화
#### 입력
- `원본데이터/DataTable.json`
- `원본데이터/Blueprint.json`
- `원본데이터/AnimMontage.json`
#### 출력
- `01_스토커별_기본데이터_v2.md`
- `validated_data.json`
#### 실행 스크립트
```bash
cd 분석도구/v2
python extract_stalker_data_v2.py
python validate_stalker_data.py
python generate_stalker_docs_v2.py
```
#### 핵심 알고리즘
**1. 공격 스킬 판정 (우선순위)**:
```python
# Priority 1: NotifyName 키워드
if any(keyword in notify_name for keyword in ['AttackWithEquip', 'Projectile', 'SkillActive']):
is_attack = True
# Priority 2: CustomProperties.NotifyName
custom_notify_name = custom_props.get('NotifyName', '')
if any(keyword in custom_notify_name for keyword in ATTACK_KEYWORDS):
is_attack = True
# Priority 3: NotifyClass 키워드
if any(keyword in notify_class for keyword in ATTACK_KEYWORDS):
is_attack = True
# Priority 4: SimpleSendEvent Event Tag
if 'SimpleSendEvent' in notify_class:
event_tag = custom_props.get('Event Tag', '')
if 'Event.SkillActivate' in event_tag or 'Event.SpawnProjectile' in event_tag:
is_attack = True
```
**2. 시퀀스 길이 계산**:
```python
def calculate_sequence_length(skill_id, montage_data):
# 1. 키워드 제외 (Ready, Equipment)
# 2. 특정 몽타주 제외 (exclude_montages 설정)
# 3. 인덱스 제외 (exclude_montage_indices 설정)
# 4. 평균 계산 (average_skills 설정)
if skill_id in average_skills:
return sum(durations) / len(durations), True
else:
return sum(durations), False
```
**3. 소환체 공격 사이클**:
```python
# 순차 루프 계산 (1→2→3→1→2→3...)
total_cycle = sum(montage_durations)
cycle_count = active_duration / total_cycle
attack_count = cycle_count * len(montages)
```
#### 검증 체크리스트
- [ ] 10명 스토커 모두 추출됨
- [ ] 모든 스토커 스탯 합계 = 75
- [ ] 궁극기 10개 확인
- [ ] 공격 스킬 vs 유틸리티 분류 정확성
- [ ] 시퀀스 길이 0이 아닌 값
- [ ] 소환체 데이터 (Ifrit, Shiva)
- [ ] DoT 스킬 4개 (Poison, Burn, Bleed)
### 12.3 02단계: DPS 시나리오 비교분석
#### 목적
- 3개 DPS 시나리오 계산 (평타, 로테이션, 버스트)
- 특수 상황 분석 (DoT, 소환체, 패링)
- 신규 스토커 중심 상세 분석
#### 입력
- `validated_data.json` (01단계 출력)
- `config.py` (BaseDamage 계산 설정)
#### 출력
- `02_DPS_시나리오_비교분석_v2.md`
#### 실행 스크립트
```bash
cd 분석도구/v2
python calculate_dps_scenarios_v2.py
```
#### BaseDamage 계산식
**레벨 20, 기어스코어 400 기준**:
```python
# 물리 딜러
Physical_BaseDamage = (주스탯 + 80) × 1.20
# 주스탯: STR or DEX
# 80: 장비 보너스
# 1.20: 룬 효과 (+10% 물리 + +10% 스킬)
# 마법 딜러
Magical_BaseDamage = (INT + 80) × 1.10
# 1.10: 룬 효과 (+10% 마법)
# 탱커/서포터
Support_BaseDamage = (주스탯 + 80) × 1.00
# 생존력 중심 (피해 증가 룬 없음)
```
#### 시나리오 1: 평타 DPS
**목적**: 순수 평타만으로 지속 딜 측정
**계산식**:
```python
평타_DPS = (BaseDamage × 평타배율합계) / 콤보시간
# 예: Rio
# BaseDamage = (25 + 80) × 1.20 = 126
# 평타배율합계 = (1.0 - 0.3) + (1.0 - 0.2) + (1.0 - 0.15) = 2.15
# 콤보시간 = 1.17 + 1.33 + 1.37 = 3.87초
# 평타_DPS = (126 × 2.15) / 3.87 = 69.9
```
**특수 처리**:
```python
# Urud, Lian: Reload
평타_DPS_with_reload = 평타_DPS × (발사횟수 / (발사시간 + reload시간))
# Lian: Charging
평타_DPS_charged = (BaseDamage × 1.5) / (충전시간 + 발사시간)
```
#### 시나리오 2: 스킬 로테이션 DPS (30초)
**목적**: 스킬 + 평타 조합한 실전 DPS
**계산식**:
```python
로테이션_DPS = (30초간_총_피해량) / 30
# 스킬 사용 횟수
스킬_사용횟수 = floor((30 - castingTime) / (coolTime + 시퀀스길이))
# 평타 필러 시간
평타_필러_시간 = 30 - sum(스킬_사용시간)
```
**로테이션 규칙**:
1. 유틸리티 스킬 제외 (isUtility=True)
2. 쿨타임 짧은 순서로 우선 사용
3. 마나 관리: 0.2/초 + 룬 +70% = 0.34/초
4. 스킬 쿨타임 중 평타 사용
**DoT 피해 추가**:
```python
# Poison/Burn: 대상 MaxHP 비례
DoT_피해 = 대상_MaxHP × DoT_rate × (30 / DoT_duration)
# Bleed: 고정 피해
DoT_피해 = 고정피해 × (30 / DoT_duration)
```
**소환체 피해 추가**:
```python
# Ifrit: 20초 지속, 8.29초 사이클
Ifrit_공격횟수 = 20 / 8.29 × 3 = ~7.2
Ifrit_피해 = 7.2 × BaseDamage × 1.2
# Shiva: 60초 지속, 2.32초 사이클
Shiva_공격횟수 = 30 / 2.32 = ~12.9
Shiva_피해 = 12.9 × BaseDamage × 0.8
```
#### 시나리오 3: 버스트 DPS (10초)
**목적**: 궁극기 포함 최대 화력
**계산식**:
```python
버스트_DPS = (궁극기_피해 + 모든_스킬_피해 + 평타_피해) / 10
```
**조건**:
- 모든 스킬 쿨타임 완료 상태
- 마나 제한 무시 (풀 마나 50 + 회복)
- 최적 순서로 스킬 사용
**유틸리티 궁극기 처리**:
```python
# Lian: 폭우 (쿨타임 -50%, 15초)
버스트기간 = 10
스킬_추가사용 = 쿨타임_50%_감소로_인한_추가_발동
# Hilda: 핏빛 달 (공격력 +15, 20초)
버스트기간내_스킬피해 = (BaseDamage + 15) × 스킬배율
```
#### 특수 상황 분석
**1. DoT DPS (대상 HP별)**:
```python
DoT_DPS_table = {
'100HP': {
'Poison': 100 × 0.20 / 5 = 4 DPS,
'Burn': 100 × 0.10 / 3 = 3.33 DPS
},
'500HP': {
'Poison': 500 × 0.20 / 5 = 20 DPS,
'Burn': 500 × 0.10 / 3 = 16.67 DPS
},
'1000HP': {
'Poison': 1000 × 0.20 / 5 = 40 DPS,
'Burn': 1000 × 0.10 / 3 = 33.33 DPS
}
}
```
**2. 소환체 독립 DPS**:
```python
# Ifrit (20초 지속)
Ifrit_DPS = (BaseDamage × 1.2 × 7.2) / 20
# Shiva (60초 지속)
Shiva_DPS = (BaseDamage × 0.8 × 25.9) / 60
```
**3. 패링 시나리오 (Cazimord)**:
```python
# 패링 0%
DPS_no_parry = 기본_로테이션_DPS
# 패링 50% (5회/10회 성공)
쿨감_효과 = 섬광_3.8 + 날개베기_3.8 + 작열_6.8
추가_스킬사용 = 쿨감으로_인한_추가_발동
DPS_50_parry = 기본_DPS + 추가_스킬_DPS
# 패링 100% (10회/10회 성공)
DPS_100_parry = 기본_DPS + (추가_스킬_DPS × 2)
```
#### 출력 구조
**시나리오별 비교표**:
```markdown
## 시나리오 1: 평타 DPS
| 순위 | 스토커 | BaseDamage | 평타 DPS | 특수 처리 |
|------|--------|------------|----------|-----------|
| 1 | Rio | 126 | 69.9 | Chain Score 3스택 |
| ... |
## 시나리오 2: 스킬 로테이션 DPS (30초)
| 순위 | 스토커 | 로테이션 DPS | 주요 스킬 | DoT/소환체 |
|------|--------|--------------|-----------|------------|
| 1 | Cazimord | 221 | 섬광+날개베기+작열 | - |
| ... |
## 시나리오 3: 버스트 DPS (10초)
| 순위 | 스토커 | 버스트 DPS | 궁극기 | 특징 |
|------|--------|------------|--------|------|
| 1 | Cazimord | 256 | 칼날폭풍 (10.0배) | 단일 최강 |
| ... |
```
**신규 스토커 상세 분석** (Cazimord):
```markdown
## 신규 스토커 상세 분석: Cazimord
### 평타 DPS
- 3타 콤보: ...
- 타임라인: 0초 1타 → 1.67초 2타 → 3.57초 3타 → 5.44초 반복
### 30초 로테이션
**타임라인**:
```
0.0초: 작열 시전 (2초 casting + 2.43초)
4.43초: 평타 콤보 시작
9.87초: 섬광 (1.73초)
11.6초: 날개베기 (2.00초)
13.6초: 평타 콤보
...
```
**패링 영향**:
- 패링 0%: 221 DPS
- 패링 50%: 245 DPS (+10.9%)
- 패링 100%: 268 DPS (+21.3%)
### 버스트 DPS (10초)
**궁극기 칼날폭풍**:
- 12연타: 80% × 10회 + 100% × 2회 = 10.0배
- 타임라인: ...
```
#### 검증 체크리스트
- [ ] 10명 스토커 모두 3개 시나리오 계산됨
- [ ] BaseDamage 계산 정확성
- [ ] 평타 배율 합계 정확성
- [ ] 스킬 로테이션 마나 부족 없음
- [ ] DoT 피해 대상 HP별 표시
- [ ] 소환체 공격 횟수 정확성
- [ ] 신규 스토커 상세 타임라인 포함
### 12.4 03단계: 역할별 차별화
#### 목적
- 5개 역할군 비교 (전사, 원거리, 마법사, 암살자, 서포터)
- 동일 역할 내 차별화 포인트 분석
#### 입력
- `02_DPS_시나리오_비교분석_v2.md` (DPS 데이터)
- `validated_data.json` (스킬 데이터)
#### 출력
- `03_역할별_차별화_v2.md`
#### 역할군 분류
| 역할군 | 스토커 | 인원 |
|--------|--------|------|
| **전사** | Hilda, Baran, Cazimord | 3명 |
| **원거리** | Urud, Lian | 2명 |
| **마법사** | Nave, Rene | 2명 |
| **암살자** | Rio, Sinobu | 2명 |
| **서포터** | Clad | 1명 |
#### 분석 항목
**각 역할군마다**:
1. **공통점**: 무기, 공격타입, 룬효과, 평타콤보
2. **스탯 비교**: STR/DEX/INT/CON/WIS, BaseDamage, DPS
3. **스킬 구성 비교**: 쿨타임, 배율, 특수효과
4. **차별화 포인트**: 핵심 시스템, 강점/약점, 플레이스타일
#### 출력 구조
```markdown
## 1. 전사 (Warriors) - 3명 비교
### 공통점
| 항목 | 공통 특성 |
|------|----------|
| **무기 타입** | 근접 무기 |
| **공격 타입** | Physical 피해 |
### 스탯 비교
| 스토커 | STR | DEX | BaseDamage | 평타 DPS | 로테이션 DPS |
|--------|-----|-----|------------|----------|--------------|
| Hilda | 20 | 15 | 120 | 69.9 | 117 |
| Baran | 25 | 10 | 126 | 84.2 | 128 |
| Cazimord | 15 | 25 | 126 | 91.5 | 221 |
### 차별화 포인트
#### Hilda - 방어형 탱커
- **핵심 시스템**: Blocking
- **강점**: 최고 생존력
- **약점**: 낮은 DPS
- **플레이스타일**: ...
#### Baran - CC 특화 전사
...
#### Cazimord - 고숙련도 DPS 전사
...
```
#### 검증 체크리스트
- [ ] 5개 역할군 모두 분석됨
- [ ] 각 역할군 공통점 명시
- [ ] 스탯/DPS 비교표 정확성
- [ ] 차별화 포인트 명확함
- [ ] 신규 스토커 역할 위치 명확
### 12.5 04단계: 밸런스 티어 및 개선안
#### 목적
- 종합 티어 평가 (OP/S+/S/A/B)
- DPS, 유틸리티별 티어
- 밸런스 개선안 제시
#### 입력
- `02_DPS_시나리오_비교분석_v2.md` (DPS 데이터)
- `03_역할별_차별화_v2.md` (역할 분석)
#### 출력
- `04_밸런스_티어_및_개선안_v2.md`
#### 티어 기준
**종합 티어** (DPS + 유틸리티):
- **OP** (Overpowered): 과도한 성능, 즉시 조정 필요
- **S+**: 최상위, 역할 모델
- **S**: 상위, 경쟁력 우수
- **A**: 중상위, 밸런스 양호
- **B**: 중하위, 개선 필요
**평가 지표**:
```python
종합_점수 = (로테이션_DPS × 0.4) + (버스트_DPS × 0.3) + (유틸리티_점수 × 0.3)
# 유틸리티 점수 (0~20점)
유틸리티_점수 = CC점수 + 생존력점수 + 기동성점수 + 팀기여점수
```
#### 출력 구조
```markdown
## 1. 종합 티어표
| 티어 | 스토커 | 로테이션 DPS | 유틸리티 | 주요 강점 | 밸런스 상태 |
|------|--------|--------------|----------|-----------|-------------|
| **OP** | Rio | 268 | 13점 | 압도적 DPS | ⚠️ 너프 필요 |
| **S+** | Cazimord | 221 | 15점 | 버스트 1위 | ✅ 양호 |
| ... |
## 2. DPS 티어별 분석
### OP 티어 (너프 필요)
**Rio**:
- 현재 DPS: 268
- 문제점: 2위보다 +21% 과다
- 개선안:
1. Chain Score 배율 감소 (150% → 100%)
2. 연속 찌르기 쿨타임 증가 (3.5초 → 5초)
3. 예상 DPS: 220 (-18%)
### B 티어 (버프 필요)
**Urud**:
- 현재 DPS: 82
- 문제점: Reload 페널티 과다
- 개선안:
1. 재장전 시간 감소 (2.0초 → 1.5초)
2. 탄약 증가 (6발 → 8발)
3. 예상 DPS: 105 (+28%)
```
#### 검증 체크리스트
- [ ] 10명 모두 티어 배정됨
- [ ] 티어 기준 명확함
- [ ] DPS 격차 분석 정확성
- [ ] 개선안 구체적 (수치 포함)
- [ ] 예상 DPS 재계산됨
### 12.6 전체 프로세스 검증
#### 일관성 체크
- [ ] 01~04단계 스토커 순서 동일
- [ ] 01단계 BaseDamage = 02단계 BaseDamage
- [ ] 02단계 DPS = 03단계 DPS
- [ ] 03단계 분석 = 04단계 티어 근거
#### 데이터 무결성
- [ ] 중간 파일 존재 (intermediate_data.json, validated_data.json)
- [ ] 모든 스킬 ID 일치
- [ ] 소환체/DoT 데이터 누락 없음
#### 문서 품질
- [ ] Markdown 형식 정확성
- [ ] 표 정렬 일관성
- [ ] 계산식 명시
- [ ] 출처 표시
---
## 📊 13. v2.1 업데이트 - 콤보 캔슬 시스템 발견 (2025-10-28)
### 13.1 주요 발견사항
#### 13.1.1 콤보 캔슬 시스템 (Game Changer!)
**발견 배경**:
- AnimMontage.json 분석 중 `ANS_DisableBlockingState_C` 노티파이 발견
- 특정 스토커의 평타 모션에서 조기 캔슬이 가능함을 확인
**노티파이 구조**:
```json
{
"NotifyName": "ANS_DisableBlockingState_C",
"TriggerTime": 2.73,
"Duration": 1.0,
"NotifyType": "NotifyState",
"NotifyStateClass": "ANS_DisableBlockingState_C"
}
```
**캔슬 가능 시점 계산**:
```python
cancellable_time = TriggerTime + Duration
# 예: 2.73 + 1.0 = 3.73초
# 원본 actualDuration: 4.57초
# 캔슬 시간: 3.73초
# 시간 단축: (4.57 - 3.73) / 4.57 = 18.4% ≈ 19%
```
**적용 대상 스토커**:
| 스토커 | 무기 타입 | 원본 시간 | 캔슬 시간 | 단축율 | DPS 변화 |
|--------|-----------|-----------|-----------|--------|----------|
| **클라드** | oneHandWeapon (Mace) | 4.17초 | 1.84초 | **56%** 🔥 | 52.9 → 125.5 (+137%) |
| **힐다** | weaponShield | 4.57초 | 3.69초 | 19% | 87.3 → 107.3 (+23%) |
| **바란** | twoHandWeapon | 5.53초 | 4.48초 | 19% | 79.0 → 90.4 (+14%) |
**영향도 분석**:
- **클라드**: 서포터임에도 **평타 DPS 1위** 달성 (125.5)
- **힐다**: 탱커 중 최고 DPS 달성 (107.3)
- **바란**: 중상위권으로 상승 (90.4)
#### 13.1.2 바란 궁극기 시전시간 정정
**문제점**:
- DT_Skill에서 castingTime: 10초로 표기
- 실제로는 즉발이 아님
**해결**:
AnimMontage.json의 `AN_SimpleSendEvent_C` 노티파이 확인
```json
{
"NotifyClass": "AN_SimpleSendEvent_C",
"TriggerTime": 1.2927,
"CustomProperties": {
"Event Tag": "(TagName=\"Ability.Attack.Ready\")"
}
}
```
**정정 내용**:
- **실제 시전시간**: 1.29초 (AN_SimpleSendEvent TriggerTime)
- **10초의 의미**: 최대 홀딩 시간 (대검을 들고 있으면서 타이밍 조절 가능)
- **DPS 영향**: 버스트 DPS 128.5 → 123.3 (-4%)
**스크립트 자동 처리**:
```python
# extract_stalker_data_v2.py (line 669-679)
if skill_id == 'SK130301': # 바란 궁극기
for montage_data in skill_data['montageData']:
for notify in montage_data.get('allNotifies', []):
if 'SimpleSendEvent' in notify.get('NotifyClass', ''):
event_tag = notify.get('CustomProperties', {}).get('Event Tag', '')
if 'Ability.Attack.Ready' in event_tag:
trigger_time = notify.get('TriggerTime', 0)
skill_data['castingTime'] = round(trigger_time, 2)
break
```
#### 13.1.3 레네 궁극기 실전 필수화
**배경**:
- 초기 분석: 궁극기 제외 (순수 DPS 우선)
- 실제 플레이: 흡혈 50% 효과로 생존 필수
**궁극기 효과**:
- 마석 '붉은 축제' (SK160301)
- 시전시간: 1.5초
- 지속시간: 20초
- 효과: 자신과 아군 모든 공격에 흡혈 50%
**DPS 트레이드오프**:
- **이전 (궁극기 제외)**: 186.4 DPS (15초 버스트 1위)
- **현재 (궁극기 포함)**: 136.7 DPS (15초 버스트 4위)
- **감소율**: -26.6%
- **보상**: 생존력 확보 (실전 필수)
### 13.2 버스트 시나리오 확대 (10초 → 15초)
**변경 이유**:
1. 궁극기 시전시간 포함 시 10초 부족
2. 대부분 궁극기 지속시간 15초 이상
3. 실전 버스트 상황에 더 부합
**새로운 버스트 DPS 순위** (15초):
| 순위 | 스토커 | 15초 DPS | 궁극기 | 주요 변화 |
|------|--------|----------|--------|----------|
| 1 | 카지모르드 | 165.1 | ✅ | 2위 → 1위 (Parrying + 궁극기) |
| 2 | 리오 | 146.9 | ✅ | 변동 없음 |
| 3 | 시노부 | 142.7 | ❌ | 변동 없음 (궁극기 제외) |
| 4 | **레네** | 136.7 | ✅ | **1위 → 4위** (흡혈 생존력) |
| 5 | 클라드 | 125.4 | ❌ | 변동 없음 (콤보 캔슬) |
| 6 | **바란** | 123.3 | ✅ | **5위 → 6위** (시전시간 정정) |
### 13.3 config.py 업데이트 내용
**추가된 설정**:
```python
# 콤보 캔슬 시스템 (v2.1)
COMBO_CANCEL_STALKERS = {
'hilda': {
'weapons': ['weaponShield'],
'patterns': ['AM_PC_Hilda_B_Attack_W01_'],
'time_reduction': 0.19, # 19% 시간 단축
'description': '3타 콤보 캔슬 (4.57s → 3.69s)'
},
'baran': {
'weapons': ['twoHandWeapon'],
'patterns': ['AM_PC_Baran_B_Attack_W01_'],
'time_reduction': 0.19,
'description': '평타 콤보 캔슬 (5.53s → 4.48s)'
},
'clad': {
'weapons': ['oneHandWeapon'],
'patterns': ['AM_PC_Clad_Base_Attack_Mace'],
'time_reduction': 0.56, # 56% 시간 단축 (극적!)
'description': '평타 콤보 캔슬 (4.17s → 1.84s)'
}
}
# 특수 궁극기 처리 (v2.1)
SPECIAL_ULTIMATE_HANDLING = {
'SK130301': { # 바란 - 일격분쇄
'stalker': 'baran',
'use_an_simplesendevent_time': True,
'event_tag': 'Ability.Attack.Ready',
'description': 'AN_SimpleSendEvent 시점(1.29초)이 실제 발동 시간'
}
}
```
### 13.4 extract_stalker_data_v2.py 업데이트
**콤보 캔슬 추출 로직** (line 277-415):
```python
def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
# 콤보 캔슬 적용 대상 스토커 및 패턴
CANCEL_TARGETS = {
'hilda': ['AM_PC_Hilda_B_Attack_W01_'],
'baran': ['AM_PC_Baran_B_Attack_W01_'],
'clad': ['AM_PC_Clad_Base_Attack_Mace']
}
for montage in pc_montages:
# 콤보 캔슬 적용 대상 판별
is_cancel_target = False
for stalker_name, patterns in CANCEL_TARGETS.items():
for pattern in patterns:
if pattern in asset_name:
is_cancel_target = True
break
# ANS_DisableBlockingState_C에서 캔슬 시간 추출
if is_cancel_target and 'ANS_DisableBlockingState' in notify_state_class:
trigger_time = notify.get('TriggerTime', 0)
duration = notify.get('Duration', 0)
cancellable_time = trigger_time + duration
```
**바란 궁극기 특수 처리** (line 669-679):
```python
# 바란 궁극기 특수 처리: AN_SimpleSendEvent 시점을 castingTime으로 사용
if skill_id == 'SK130301': # 바란 궁극기 '일격분쇄'
for montage_data in skill_data['montageData']:
for notify in montage_data.get('allNotifies', []):
if 'SimpleSendEvent' in notify.get('NotifyClass', ''):
event_tag = notify.get('CustomProperties', {}).get('Event Tag', '')
if 'Ability.Attack.Ready' in event_tag:
trigger_time = notify.get('TriggerTime', 0)
skill_data['castingTime'] = round(trigger_time, 2)
print(f" [INFO] {skill_id}: castingTime 오버라이드 {skill_data['castingTime']}초")
break
```
### 13.5 02단계 문서 업데이트 내용
**시나리오 1 - 평타 DPS 순위 변화**:
```markdown
| 순위 | 스토커 | Raw DPS | 특징 |
|------|--------|---------|------|
| 1 | **클라드** | **125.5** | ⚡ 콤보 캔슬 (+137% DPS!) |
| 2 | **힐다** | **107.3** | ⚡ 콤보 캔슬 (+23%) |
| 3 | 시노부 | 97.83 | 표창 충전 시스템 |
| 4 | **바란** | **90.4** | ⚡ 콤보 캔슬 (+14%) |
```
**시나리오 2 - 30초 로테이션 변화**:
- 클라드: 60.1 → 133.6 DPS (+122%, **7위 → 3위**)
- 힐다: 92.1 → 114.1 DPS (+24%)
- 바란: 97.9 → 111.4 DPS (+14%)
**시나리오 3 - 15초 버스트 (신설)**:
- 기존 10초 → 15초로 확대
- 궁극기 사용 정책 명확화:
- 기본: 0초 시점 사용
- 예외: 클라드/시노부 (방어 궁극기 제외)
- 특수: 카지모르드 (작열 → 섬광 → 궁극기)
**중간 결론 섹션 신설**:
- DPS 기준 종합 티어표 (3개 시나리오 통합 평가)
- 밸런스 개선 제안 (C/B티어 수치 조정안)
### 13.6 검증 체크리스트
**v2.1 검증 항목**:
- [x] 콤보 캔슬 시스템 config.py 추가
- [x] 바란 궁극기 특수 처리 스크립트 반영
- [x] extract_stalker_data_v2.py 업데이트
- [x] 01 문서 바란 궁극기 시전시간 정정
- [x] 02 문서 3개 시나리오 콤보 캔슬 반영
- [x] 02 문서 15초 버스트 시나리오 재작성
- [x] 02 문서 중간 결론 섹션 작성
- [x] 종합 티어표 3개 시나리오 통합 평가
- [x] 밸런스 개선 제안 (리안, 우르드, 네이브)
---
**작성자**: AI-assisted Analysis Team
**최종 업데이트**: 2025-10-28
**버전**: 2.1