Files
DS-Combat_analy/ARCHITECTURE.md
2025-10-27 17:04:37 +09:00

21 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"

⚠️ 주의사항

  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 프로젝트 문서


작성자: AI-assisted Analysis Team 최종 업데이트: 2025-10-27 버전: 1.0