Files
DS-Combat_analy/legacy/ARCHITECTURE.md
2025-11-05 11:09:16 +09:00

42 KiB
Raw Permalink Blame History

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"

⚠️ 주의사항

  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 구조 (평타 추출)

{
  "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)

특징:

  • 스킬 자체는 소환 동작
  • 소환된 정령이 공격함 → 소환체 데이터 별도 처리

처리 방법:

  1. 소환 스킬 자체는 skillDamageRate에 따라 공격/유틸리티 판정
  2. 소환체 데이터는 Blueprint.json에서 추출
  3. 문서에서는 "소환체" 섹션 분리

🔧 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 노티파이 없음

해결:

  1. SimpleSendEvent의 CustomProperties 확인
  2. Event.SkillActivate, Event.SpawnProjectile 체크
  3. 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 별도 계산 로직 누락

해결:

  1. config.py에 DoT 스킬 정의
  2. isDot 플래그 추가
  3. DoT 종합 비교 테이블 생성
  4. 개별 스킬에 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 프로젝트 문서


📊 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

실행 스크립트

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(스킬_사용시간)

로테이션 규칙:

  1. 유틸리티 스킬 제외 (isUtility=True)
  2. 쿨타임 짧은 순서로 우선 사용
  3. 마나 관리: 0.2/초 + 룬 +70% = 0.34/초
  4. 스킬 쿨타임 중 평타 사용

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명

분석 항목

각 역할군마다:

  1. 공통점: 무기, 공격타입, 룬효과, 평타콤보
  2. 스탯 비교: STR/DEX/INT/CON/WIS, BaseDamage, DPS
  3. 스킬 구성 비교: 쿨타임, 배율, 특수효과
  4. 차별화 포인트: 핵심 시스템, 강점/약점, 플레이스타일

출력 구조

## 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초)

변경 이유:

  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 업데이트 내용

추가된 설정:

# 콤보 캔슬 시스템 (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