42 KiB
ARCHITECTURE.md
던전 스토커즈 전투 분석 시스템 - 기술 아키텍처 문서
- 목적: 데이터 구조, 추출 로직, 판정 알고리즘 등 구현 세부사항 문서화
- 대상: 개발자, 분석 스크립트 유지보수자
- 최종 업데이트: 2025-10-27
📁 1. 데이터 소스 구조
1.1 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" |
몽타주 경로 추출
# 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"
⚠️ 주의사항
-
descValues 소수점 오류: JSON 추출 과정에서
3.799999952316284같은 값 발생- 해결: 모든 float 값을
round(val, 2)로 소수점 둘째자리 반올림
- 해결: 모든 float 값을
-
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 구조 (평타 추출)
{
"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'"
]
}
}
}
추출 방법:
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 구조 (공격 판정 핵심)
각 노티파이는 다음 구조를 가집니다:
{
"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 구조:
{
"Event Tag": "(TagName=\"Event.SkillActivate\")",
"NotifyColor": "(B=200,G=200,R=255,A=255)",
"bShouldFireInEditor": ""
}
Event Tag 파싱:
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 (시퀀스 길이) 계산:
# 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 판정 알고리즘
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" 키워드 있음
- 하지만 공격 노티파이 없음 → 유틸리티로 판정
# 재장전 스킬 예외 처리
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노티파이 있음 → 공격 스킬
# 차징 후 발사하는 스킬도 공격 스킬
if 'Charging' in asset_name:
if has_projectile_notify(montage):
return True # 공격 스킬
4.2.3 소환 스킬 (공격)
스킬 ID: SK160202 (Rene Ifrit), SK160206 (Rene Shiva)
특징:
- 스킬 자체는 소환 동작
- 소환된 정령이 공격함 → 소환체 데이터 별도 처리
처리 방법:
- 소환 스킬 자체는 skillDamageRate에 따라 공격/유틸리티 판정
- 소환체 데이터는 Blueprint.json에서 추출
- 문서에서는 "소환체" 섹션 분리
🔧 5. 특수 데이터 처리
5.1 DoT (Damage over Time) 스킬
정의 위치: config.py
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)
데이터 구조:
{
"summonClass": "/Game/Blueprints/Characters/Rene/BP_Ifrit.BP_Ifrit_C",
"skillDamageRate": 1.2, // 소환체가 이 배율로 공격
"duration": 30 // 소환 지속 시간
}
문서 표시 방법:
### SK160202 정령 소환 : 화염
- **스킬 타입**: 소환
- **피해 배율**: 1.2 (정령이 대행)
- **마나**: 15
- **쿨타임**: 20초
## 소환체
### 🔥 화염 정령 (Ifrit)
- **소환 스킬**: SK160202 정령 소환 : 화염
- **공격력**: 1.2 (소환자 공격력 대행)
- **공격 속도**: [Blueprint에서 추출]
- **지속시간**: 30초
- **특수 효과**: Burn DoT (MaxHP 10%, 3초)
📐 6. DPS 계산 공식
6.1 기본 DPS
# 평타 DPS
basic_dps = attack_damage_rate / actual_duration
# 스킬 DPS
skill_dps = skill_damage_rate / (actual_duration + casting_time)
6.2 actualDuration (시퀀스 길이) 계산
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
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 최상위 구조 파악
# ❌ 잘못된 접근
for item in data: # data가 dict이면 에러
...
# ✅ 올바른 접근
assets = data.get('Assets', [])
for asset in assets:
...
7.1.2 Asset 찾기
# 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 몽타주 경로 파싱
# 전체 경로
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 파싱
# 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 소수점 반올림
# 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 디버깅 팁
# 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 노티파이 없음
해결:
- SimpleSendEvent의 CustomProperties 확인
- Event.SkillActivate, Event.SpawnProjectile 체크
- Projectile 노티파이도 공격 판정에 포함
9.2 "평타 actualDuration이 0"
원인:
- DT_Skill이 아닌 DT_CharacterAbility에서 평타 찾아야 함
- attackMontageMap 파싱 실패
- SequenceLength 또는 RateScale 데이터 누락
해결:
# 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 별도 계산 로직 누락
해결:
- config.py에 DoT 스킬 정의
- isDot 플래그 추가
- DoT 종합 비교 테이블 생성
- 개별 스킬에 DoT 상세 정보 표시
9.4 "descValues가 너무 긴 소수점"
원인:
- JSON 추출 시 float 정밀도 문제
해결:
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 언리얼 엔진 시스템
11.2 프로젝트 문서
- README.md - 프로젝트 개요 및 사용 가이드
- ../CLAUDE.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.mdvalidated_data.json
실행 스크립트
cd 분석도구/v2
python extract_stalker_data_v2.py
python validate_stalker_data.py
python generate_stalker_docs_v2.py
핵심 알고리즘
1. 공격 스킬 판정 (우선순위):
# 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. 시퀀스 길이 계산:
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. 소환체 공격 사이클:
# 순차 루프 계산 (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
실행 스크립트
cd 분석도구/v2
python calculate_dps_scenarios_v2.py
BaseDamage 계산식
레벨 20, 기어스코어 400 기준:
# 물리 딜러
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
목적: 순수 평타만으로 지속 딜 측정
계산식:
평타_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
특수 처리:
# Urud, Lian: Reload
평타_DPS_with_reload = 평타_DPS × (발사횟수 / (발사시간 + reload시간))
# Lian: Charging
평타_DPS_charged = (BaseDamage × 1.5) / (충전시간 + 발사시간)
시나리오 2: 스킬 로테이션 DPS (30초)
목적: 스킬 + 평타 조합한 실전 DPS
계산식:
로테이션_DPS = (30초간_총_피해량) / 30초
# 스킬 사용 횟수
스킬_사용횟수 = floor((30초 - castingTime) / (coolTime + 시퀀스길이))
# 평타 필러 시간
평타_필러_시간 = 30초 - sum(스킬_사용시간)
로테이션 규칙:
- 유틸리티 스킬 제외 (isUtility=True)
- 쿨타임 짧은 순서로 우선 사용
- 마나 관리: 0.2/초 + 룬 +70% = 0.34/초
- 스킬 쿨타임 중 평타 사용
DoT 피해 추가:
# Poison/Burn: 대상 MaxHP 비례
DoT_피해 = 대상_MaxHP × DoT_rate × (30초 / DoT_duration)
# Bleed: 고정 피해
DoT_피해 = 고정피해 × (30초 / DoT_duration)
소환체 피해 추가:
# 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초)
목적: 궁극기 포함 최대 화력
계산식:
버스트_DPS = (궁극기_피해 + 모든_스킬_피해 + 평타_피해) / 10초
조건:
- 모든 스킬 쿨타임 완료 상태
- 마나 제한 무시 (풀 마나 50 + 회복)
- 최적 순서로 스킬 사용
유틸리티 궁극기 처리:
# Lian: 폭우 (쿨타임 -50%, 15초)
버스트기간 = 10초
스킬_추가사용 = 쿨타임_50%_감소로_인한_추가_발동
# Hilda: 핏빛 달 (공격력 +15, 20초)
버스트기간내_스킬피해 = (BaseDamage + 15) × 스킬배율
특수 상황 분석
1. DoT DPS (대상 HP별):
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:
# Ifrit (20초 지속)
Ifrit_DPS = (BaseDamage × 1.2 × 7.2회) / 20초
# Shiva (60초 지속)
Shiva_DPS = (BaseDamage × 0.8 × 25.9회) / 60초
3. 패링 시나리오 (Cazimord):
# 패링 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)
출력 구조
시나리오별 비교표:
## 시나리오 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):
## 신규 스토커 상세 분석: 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명 |
분석 항목
각 역할군마다:
- 공통점: 무기, 공격타입, 룬효과, 평타콤보
- 스탯 비교: STR/DEX/INT/CON/WIS, BaseDamage, DPS
- 스킬 구성 비교: 쿨타임, 배율, 특수효과
- 차별화 포인트: 핵심 시스템, 강점/약점, 플레이스타일
출력 구조
## 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: 중하위, 개선 필요
평가 지표:
종합_점수 = (로테이션_DPS × 0.4) + (버스트_DPS × 0.3) + (유틸리티_점수 × 0.3)
# 유틸리티 점수 (0~20점)
유틸리티_점수 = CC점수 + 생존력점수 + 기동성점수 + 팀기여점수
출력 구조
## 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노티파이 발견 - 특정 스토커의 평타 모션에서 조기 캔슬이 가능함을 확인
노티파이 구조:
{
"NotifyName": "ANS_DisableBlockingState_C",
"TriggerTime": 2.73,
"Duration": 1.0,
"NotifyType": "NotifyState",
"NotifyStateClass": "ANS_DisableBlockingState_C"
}
캔슬 가능 시점 계산:
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 노티파이 확인
{
"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%)
스크립트 자동 처리:
# 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초)
변경 이유:
- 궁극기 시전시간 포함 시 10초 부족
- 대부분 궁극기 지속시간 15초 이상
- 실전 버스트 상황에 더 부합
새로운 버스트 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 업데이트 내용
추가된 설정:
# 콤보 캔슬 시스템 (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):
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):
# 바란 궁극기 특수 처리: 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 순위 변화:
| 순위 | 스토커 | 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 검증 항목:
- 콤보 캔슬 시스템 config.py 추가
- 바란 궁극기 특수 처리 스크립트 반영
- extract_stalker_data_v2.py 업데이트
- 01 문서 바란 궁극기 시전시간 정정
- 02 문서 3개 시나리오 콤보 캔슬 반영
- 02 문서 15초 버스트 시나리오 재작성
- 02 문서 중간 결론 섹션 작성
- 종합 티어표 3개 시나리오 통합 평가
- 밸런스 개선 제안 (리안, 우르드, 네이브)
작성자: AI-assisted Analysis Team 최종 업데이트: 2025-10-28 버전: 2.1