# 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