리뉴얼

This commit is contained in:
Gnill82
2025-10-27 17:04:37 +09:00
parent faef6ce8bd
commit af7de76bc0
57 changed files with 479079 additions and 633159 deletions

727
ARCHITECTURE.md Normal file
View File

@ -0,0 +1,727 @@
# 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 변수 검증 계획
---
**작성자**: AI-assisted Analysis Team
**최종 업데이트**: 2025-10-27
**버전**: 1.0

View File

@ -455,8 +455,8 @@ if (Armor / ArmorMax > 0.5) {
- Armor 공식: `MaxArmor*0.1*(1-DOTReduceRate)*(1-FireResistanceInc)/10` - Armor 공식: `MaxArmor*0.1*(1-DOTReduceRate)*(1-FireResistanceInc)/10`
- 10초 동안 적용되어 총 최대 체력/아머의 10% 피해 - 10초 동안 적용되어 총 최대 체력/아머의 10% 피해
**출혈 Bleed** (미구현) **출혈 Bleed**
- 유지 시간 동안 체력에 n만큼의 피해를 준다. - 10초 동안 1초 간격으로 체력이 -2씩 감소한다. 총 -20 체력. 출혈 저항이 없기 때문에 -2 감소는 절대값.
- 중독과의 차이점은 상대방의 최대 체력과 상관없이 총 피해량이 정해져 있다는 점. - 중독과의 차이점은 상대방의 최대 체력과 상관없이 총 피해량이 정해져 있다는 점.
**감전 ElectricShock** (미구현) **감전 ElectricShock** (미구현)

View File

@ -0,0 +1,909 @@
# 03. 스토커별 기본 데이터 (v2)
## 데이터 소스
- `DT_CharacterStat`: 기본 스탯, 스킬 목록
- `DT_CharacterAbility`: 평타 몽타주
- `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 시전시간, 효과)
- `Blueprint`: 스킬 변수 (ActivationOrderGroup 등)
- `AnimMontage`: 타이밍 및 공격 노티파이, 실제 발사 시점
## 검증 상태
- ✅ 모든 데이터는 최신 JSON (2025-10-24 15:58:55)에서 추출
- ✅ 교차 검증 완료
- ✅ 출처 명시 (각 데이터 필드별)
## DPS 계산 시 고려사항
- **시전시간**: 스킬 사용 시 시전시간(CastingTime)이 추가됨
- **실제 공격 시점**: 원거리 스킬(우르드, 리안)의 경우 몽타주 시간보다 빠르게 공격 가능
- **DoT 데미지**: DoT(Damage over Time) 스킬은 대상 HP에 비례하여 지속 피해 발생 (구체적 계산은 다음 챕터 참조)
---
## 10명 스토커 종합 비교표
| 스토커 | 직업 | STR | DEX | INT | CON | WIS | 궁극기 | 장착 무기 | 평타 |
|--------|------|-----|-----|-----|-----|-----|--------|-----------|------|
| **Hilda (힐다)** | 전사 | 20 | 15 | 10 | 20 | 10 | ⭐ | WeaponShield, Heavy, Light | 3타 |
| **Urud (우르드)** | 원거리 | 15 | 20 | 10 | 15 | 15 | ⭐ | Bow, Light, Cloth | 1타 |
| **Nave (네이브)** | 마법사 | 10 | 10 | 25 | 10 | 20 | ⭐ | Staff, Light, Cloth | 2타 |
| **Baran (바란)** | 전사 | 25 | 10 | 5 | 25 | 10 | ⭐ | TwoHandWeapon, Heavy, Light | 3타 |
| **Rio (리오)** | 암살자 | 15 | 25 | 10 | 15 | 10 | ⭐ | ShortSword, Cloth, Light | 3타 |
| **Clad (클라드)** | 성직자 | 15 | 10 | 10 | 20 | 20 | ⭐ | Mace, Heavy, Light | 2타 |
| **Rene (레네)** | 소환사 | 10 | 10 | 20 | 10 | 25 | ⭐ | Staff, Light, Cloth | 3타 |
| **Sinobu (시노부)** | 닌자 | 10 | 25 | 10 | 15 | 15 | ⭐ | ShortSword, Cloth, Light | 2타 |
| **Lian (리옌)** | 레인저 | 10 | 20 | 10 | 15 | 20 | ⭐ | Bow, Light, Cloth | 1타 |
| **Cazimord (카지모르드)** | 전사 | 15 | 25 | 10 | 15 | 10 | ⭐ | WeaponShield, Light, Cloth | 3타 |
**특징**:
- **모든 스토커가 궁극기 보유**
- 모든 스토커 스탯 합계: 75 포인트 (균형)
- HP/MP 동일: 100/50
- 마나 회복: 0.2/초 (전원 동일)
---
## 궁극기 종합 비교
| 스토커 | 궁극기 이름 | 타입 | 피해배율 | 지속/시전 | 주요 효과 |
|--------|-------------|------|----------|-----------|----------|
| **Hilda (힐다)** | 마석 ‘핏빛 달’ | Normal | 0.5 | 20초 / 2초 | 마석의 힘을 해방하여 20초 동안 공격력 15, 방어력 25 증가합니다.... |
| **Urud (우르드)** | 마석 ‘폭쇄’ | Normal | 1 | 15초 / 2초 | 마석의 힘을 해방하여 15초 동안 화살에 범위 피해 효과를 부여합니다. 적중된 대상은 30% 확률로 화상에 걸립니다.... |
| **Nave (네이브)** | 마석 ‘해방’ | MagicalSkill | 1 | 5초 / 2초 | 마석의 힘으로 5초 동안 적을 관통하는 광선을 발사합니다. 광선은 0.5초 간격마다 100%의 마법 피해를 입힙니다.... |
| **Baran (바란)** | 마석 '일격분쇄' | PhysicalSkill | 1.7 | 2초 / 10초 | 대검을 내리찍어 4m의 균열을 생성합니다. 균열 범위 내 170%의 물리 피해를 주며, 적중된 대상은 기절합니다.... |
| **Rio (리오)** | 마석 ‘민감’ | Normal | 0.3 | 15초 / 2초 | 마석의 힘을 개방하여 연계 점수 3점을 획득하고, 15초 동안 은신 및 투시 효과를 획득합니다. 대상의 뒤를 공격 시 '약점' 판정이 적용됩니다.... |
| **Clad (클라드)** | 마석 ‘황금’ | Normal | 300 | 6초 / 0.55초 | 마석의 힘을 해방하여 5초 동안 자신과 아군에게 300의 보호막을 생성합니다.... |
| **Rene (레네)** | 마석 ‘붉은 축제’ | MagicalSkill | 50 | 20초 / 2초 | 마석의 힘을 해방하여 20초 동안 자신과 아군의 모든 공격에 흡혈 효과를 부여합니다. 피해의 50%만큼 체력을 회복합니다.... |
| **Sinobu (시노부)** | 마석 '반환' | Normal | 0 | 7초 / 0초 | 마석의 힘을 개방하여 7초 동안 전방의 투사체 공격을 튕겨내고 근접 공격을 막아냅니다.... |
| **Lian (리옌)** | 마석 '폭우' | PhysicalSkill | 50 | 15초 / 1.5초 | 마석의 힘을 개방하여 15초 동안 화살을 소모하지 않으며, 쿨타임이 50% 감소합니다.... |
| **Cazimord (카지모르드)** | 마석 '칼날폭풍' | PhysicalSkill | 0.8 | 15초 / 2초 | 마석의 힘을 빌려 빠르게 정면을 12회 공격해 각각 80%의 물리 피해를 입힙니다. 마지막 2회의 타격은 100%의 물리 피해를 입힙니다. 시전 중에는 천천히 이동할 수 있지만, ... |
---
## DoT 스킬 종합 비교
다음 스킬들은 DoT(Damage over Time) 효과가 있으며, **DPS 계산 시 추가 지속 피해를 고려해야 합니다**.
| 스토커 | 스킬 이름 | DoT 타입 | 기본 피해 | DoT 피해 | 지속시간 |
|--------|----------|----------|----------|----------|----------|
| **Urud (우르드)** | 독성 화살 | Poison | 1 | 대상 MaxHP의 20% | 5초 |
| **Rene (레네)** | 독기 화살 | Bleed | 1 | 고정 20 피해 | 5초 |
| **Cazimord (카지모르드)** | 섬광 | Burn | 0.5 | 대상 MaxHP의 10% | 3초 |
| **Rene (레네)** | 정령 소환 : 화염 | Burn | 1.2 | 대상 MaxHP의 10% | 3초 |
**주의사항**:
- DoT 피해는 대상의 HP에 비례하므로, 적의 체력에 따라 실제 피해량이 달라집니다.
- 구체적인 DoT DPS 계산 방법은 다음 챕터에서 다룹니다.
- 위 표의 '기본 피해'는 스킬의 skillDamageRate입니다.
---
## 1. Hilda (힐다) - 탱커
### 기본 정보
- **역할**: 탱커
- **주 스탯**: STR 20, CON 20
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: WeaponShield, Heavy, Light
- **평타**: weaponShield 3타
### 평타 상세 정보
**weaponShield** (3타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Hilda_B_Attack_W01_01 | 1.60 | 0.0 | |
| 2 | AM_PC_Hilda_B_Attack_W01_02 | 1.60 | +5.0 | |
| 3 | AM_PC_Hilda_B_Attack_W01_03 | 1.37 | -5.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK100201 칼날 격돌**
- **타입**: PhysicalSkill / **속성**: Lightning
- **피해 배율**: 1.3
- **쿨타임**: 6초 / **마나**: 11
- **몽타주**:
1. AM_PC_Hilda_B_Skill_Ready (5.43초) [준비]
2. AM_PC_Hilda_B_Skill_SwordStrike (1.80초)
- **시퀀스 길이**: 1.80초
- **설명**: 검을 휘둘러 130%만큼 번개 속성 물리 피해를 입힙니다. 적중된 대상은 잠시 경직됩니다.
2. **SK100202 반격**
- **타입**: PhysicalSkill
- **피해 배율**: 0.8
- **쿨타임**: 4초 / **마나**: 10
- **몽타주**: AM_PC_Hilda_B_Skill_Counter
- **시퀀스 길이**: 2.81초
- **설명**: 방패를 들어 5초 동안 반격 자세를 취합니다. 반격 성공 시 80%만큼 물리 피해를 줍니다.
3. **SK100204 도발**
- **타입**: Normal
- **피해 배율**: 250
- **쿨타임**: 10초 / **마나**: 8
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Hilda_B_Skill_Provoke
- **시퀀스 길이**: 2.00초
- **설명**: 주변 5m 내의 몬스터를 도발하여 위협 수치를 획득하고 대상의 이동속도를 3초 동안 25% 감속시킵니다.10초 동안 방어력이 15 증가합니다.
**서브 스킬**:
**SK100101 방패 방어**
- **타입**: Normal
- **피해 배율**: 1
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Hilda_B_Skill_Blocking
- **시퀀스 길이**: 4.34초
- **설명**: 방패로 공격을 방어합니다.방어 유지 시 지구력이 소모됩니다.
**궁극기**:
**SK100301 마석 ‘핏빛 달’**
- **타입**: Normal
- **피해 배율**: 0.5
- **시전시간**: 2초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Hilda_B_Skill_BloodMoon (1.50초)
2. AM_PC_Hilda_B_Equipment (0.67초) [장비]
- **시퀀스 길이**: 1.50초
- **설명**: 마석의 힘을 해방하여 20초 동안 공격력 15, 방어력 25 증가합니다.
---
## 2. Urud (우르드) - 원거리 딜러
### 기본 정보
- **역할**: 원거리 딜러
- **주 스탯**: DEX 20, STR 15
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: Bow, Light, Cloth
- **평타**: bow 1타
### 평타 상세 정보
**bow** (1타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Urud_Base_B_Attack_N | 3.28 | 0.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK110205 다발 화살**
- **타입**: PhysicalSkill
- **피해 배율**: 0.9
- **쿨타임**: 7초 / **마나**: 14
- **몽타주**: AM_PC_Urud_Base_B_Skill_MultiArrow
- **시퀀스 길이**: 1.62초
- **설명**: 3발의 화살을 동시에 발사하여 각각 90%만큼 물리 피해를 입힙니다. 화살 3개 소모합니다.
2. **SK110204 독성 화살**
- **타입**: PhysicalSkill / **속성**: Poison
- **피해 배율**: 1
- **쿨타임**: 7초 / **마나**: 9
- ⚠️ **Poison 상태이상 유발**: 대상 MaxHP의 20% (5초간)
- 💡 **DoT 피해는 대상 HP에 비례** (구체적 DPS는 다음 챕터 참조)
- **몽타주**: AM_PC_Urud_Base_B_Skill_PoisonArrow
- **시퀀스 길이**: 1.62초
- **설명**: 화살에 독을 발라 발사합니다. 적중된 대상은 중독됩니다. 화살을 1개 소모합니다.
3. **SK110201 덫 설치**
- **타입**: Normal
- **쿨타임**: 5초 / **마나**: 9 / **시전시간**: 2초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Urud_B_Skill_MakeTrap (6.15초)
2. AM_PC_Urud_B_Equipment (1.00초) [장비]
- **시퀀스 길이**: 6.15초
- **설명**: 60초 동안 유지되는 덫을 최대 4개 설치할 수 있습니다. 덫을 밟은 대상은 기절합니다.
4. **SK110207 재장전**
- **타입**: Normal
- **피해 배율**: 1
- **시전시간**: 5초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Urud_B_Reload (2.53초)
2. AM_PC_Urud_B_Equipment (1.00초) [장비]
- **시퀀스 길이**: 2.53초
- **설명**: 화살을 화살통에 장전 합니다.
**서브 스킬**:
**SK110101 화살 찌르기**
- **타입**: PhysicalSkill
- **피해 배율**: 0.7
- **몽타주**: AM_PC_Urud_Base_B_Skill_ArrowStab
- **시퀀스 길이**: 1.11초
- **설명**: 화살로 찔러 75%만큼 물리 피해를 입힙니다. 적중 시 다음 일반 공격이 50% 증가합니다.
**궁극기**:
**SK110301 마석 ‘폭쇄’**
- **타입**: Normal
- **피해 배율**: 1
- **시전시간**: 2초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Urud_Base_B_Skill_Explosion (1.50초)
2. AM_PC_Urud_B_Equipment (1.00초) [장비]
- **시퀀스 길이**: 1.50초
- **설명**: 마석의 힘을 해방하여 15초 동안 화살에 범위 피해 효과를 부여합니다. 적중된 대상은 30% 확률로 화상에 걸립니다.
---
## 3. Nave (네이브) - 광역 마법 딜러
### 기본 정보
- **역할**: 광역 마법 딜러
- **주 스탯**: INT 25, WIS 20
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: Staff, Light, Cloth
- **평타**: staff 2타
### 평타 상세 정보
**staff** (2타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Nave_B_Attack_W01_01 | 1.60 | 0.0 | |
| 2 | AM_PC_Nave_B_Attack_W01_02 | 1.70 | 0.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK120201 마법 화살**
- **타입**: MagicalSkill
- **피해 배율**: 0.8
- **쿨타임**: 3.5초 / **마나**: 18 / **시전시간**: 2초
- **몽타주**: AM_PC_Nave_B_Skill_MagicMissile
- **시퀀스 길이**: 3.33초
- **설명**: 최대 3개의 마법 화살을 생성하여 일반 공격으로 발사합니다. 마법 화살은 각각 80%만큼 마법 피해를 입힙니다.
2. **SK120202 화염구**
- **타입**: MagicalSkill / **속성**: Fire
- **피해 배율**: 2
- **쿨타임**: 5초 / **마나**: 25 / **시전시간**: 4초
- **몽타주**: AM_PC_Nave_B_Skill_FireWall
- **시퀀스 길이**: 3.33초
- **설명**: 화염구를 생성하여 일반 공격으로 발사합니다. 화염구는 200% 화염 속성 마법 피해와 주변에 150% 추가 피해를 입힙니다.
3. **SK120206 노대바람**
- **타입**: MagicalSkill
- **피해 배율**: 0.5
- **쿨타임**: 7초 / **마나**: 9
- **몽타주**: AM_PC_Nave_B_Skill_WindForce
- **시퀀스 길이**: 1.33초
- **설명**: 강한 바람으로 밀쳐내고 50%만큼 마법 피해를 입힙니다.
**서브 스킬**:
**SK120101 마력 충전**
- **타입**: Normal
- **피해 배율**: 5
- **쿨타임**: 1초 / **시전시간**: 9999초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Nave_B_Skill_ManaRestore (2.80초)
2. AM_PC_Nave_B_Equipment (1.00초) [장비]
- **시퀀스 길이**: 2.80초
- **설명**: 시전하는 동안 1의 마나를 추가로 회복합니다.
**궁극기**:
**SK120301 마석 ‘해방’**
- **타입**: MagicalSkill
- **피해 배율**: 1
- **시전시간**: 2초
- **몽타주**: AM_PC_Nave_B_Skill_Escape4
- **시퀀스 길이**: 4.33초
- **설명**: 마석의 힘으로 5초 동안 적을 관통하는 광선을 발사합니다. 광선은 0.5초 간격마다 100%의 마법 피해를 입힙니다.
---
## 4. Baran (바란) - 고화력 전사
### 기본 정보
- **역할**: 고화력 전사
- **주 스탯**: STR 25, CON 25
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: TwoHandWeapon, Heavy, Light
- **평타**: twoHandWeapon 3타
### 평타 상세 정보
**twoHandWeapon** (3타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Baran_B_Attack_W01_01 | 1.90 | +5.0 | |
| 2 | AM_PC_Baran_B_Attack_W01_02 | 1.93 | +10.0 | |
| 3 | AM_PC_Baran_B_Attack_W01_03 | 1.73 | +5.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK130204 갈고리 투척**
- **타입**: PhysicalSkill
- **피해 배율**: 0.25
- **쿨타임**: 13초 / **마나**: 14
- **몽타주**: AM_PC_Baran_B_Skill_Pulling
- **시퀀스 길이**: 1.70초
- **설명**: 갈고리를 던져 25%만큼 물리 피해를 입히고, 대상을 끌어당깁니다. 적중된 대상은 잠시 경직됩니다.
2. **SK130203 후려치기**
- **타입**: PhysicalSkill
- **피해 배율**: 1.2
- **쿨타임**: 8초 / **마나**: 9
- **몽타주**: AM_PC_Baran_B_Skill_Smash
- **시퀀스 길이**: 1.89초
- **설명**: 대검을 크게 휘둘러 두 번 연속으로 120%만큼 물리 피해를 입힙니다.
3. **SK130206 깊게 찌르기**
- **타입**: PhysicalSkill
- **피해 배율**: 1.1
- **쿨타임**: 7초 / **마나**: 10
- **몽타주**: AM_PC_Baran_B_Skill_SwordStab
- **시퀀스 길이**: 1.75초
- **설명**: 대검을 깊게 찔러 넣어 120%만큼 물리 피해를 입힙니다. 문을 파괴할 수 있습니다. 적중된 대상은 잠시 경직됩니다.
**서브 스킬**:
**SK130101 무기 막기**
- **타입**: Normal
- **피해 배율**: 1
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Baran_B_Skill_Blocking
- **시퀀스 길이**: 3.57초
- **설명**: 무기로 공격을 방어합니다. 방어 유지 시 지구력이 소모됩니다.
**궁극기**:
**SK130301 마석 '일격분쇄'**
- **타입**: PhysicalSkill
- **피해 배율**: 1.7
- **시전시간**: 10초
- **몽타주**: AM_PC_Baran_B_Skill_RockBraker2
- **시퀀스 길이**: 1.98초
- **설명**: 대검을 내리찍어 4m의 균열을 생성합니다. 균열 범위 내 170%의 물리 피해를 주며, 적중된 대상은 기절합니다.
---
## 5. Rio (리오) - 빠른 근접 암살자
### 기본 정보
- **역할**: 빠른 근접 암살자
- **주 스탯**: DEX 25, STR 15
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: ShortSword, Cloth, Light
- **평타**: shortSword 3타
### 평타 상세 정보
**shortSword** (3타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Rio_B_Attack_W01_01 | 1.17 | -30.0 | |
| 2 | AM_PC_Rio_B_Attack_W01_02 | 1.33 | -20.0 | |
| 3 | AM_PC_Rio_B_Attack_W01_03 | 1.37 | -15.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK140201 연속 찌르기**
- **타입**: PhysicalSkill
- **피해 배율**: 1
- **쿨타임**: 3.5초 / **마나**: 9
- **몽타주**: AM_PC_Rio_B_Skill_RapidStab
- **시퀀스 길이**: 1.41초
- **설명**: 단검을 빠르게 2번 찔러 각각 100%만큼 암흑 속성 물리 피해를 입힙니다. 각 공격은 25%의 추가 치명타 확률을 가집니다. 타격당 연계 점수 1점을 획득합니다.
2. **SK140205 접근**
- **타입**: Normal
- **피해 배율**: 1
- **쿨타임**: 4초 / **마나**: 8
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Rio_B_Skill_Approach
- **시퀀스 길이**: 0.57초
- **설명**: 낮은 자세로 돌진합니다. 돌진 중 피격되지 않으며, 돌진 후 3초 동안 30%만큼 물리 피해가 증가합니다.
3. **SK140202 단검 투척**
- **타입**: PhysicalSkill
- **피해 배율**: 1
- **쿨타임**: 7초 / **마나**: 10 / **시전시간**: 1초
- **몽타주**:
1. AM_PC_Rio_B_Skill_ThrowingDagger (1.63초)
2. AM_PC_Rio_B_Skill_ThrowingDagger_E (1.20초)
- **시퀀스 길이**: 2.83초
- **설명**: 단검을 던져 100%만큼 피해를 입힙니다.
**서브 스킬**:
**SK140101 내려 찍기**
- **타입**: PhysicalSkill
- **피해 배율**: 0.7
- **몽타주**: AM_PC_Rio_B_Skill_DroppingAttack
- **시퀀스 길이**: 1.30초
- **설명**: 단검으로 내려 찍어 70%만큼 물리 피해를 입힙니다. 연계 점수에 따라 50/100/150% 추가 피해를 입힙니다.
**궁극기**:
**SK140301 마석 ‘민감’**
- **타입**: Normal
- **피해 배율**: 0.3
- **시전시간**: 2초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Rio_B_Skill_Sensitive (1.50초)
2. AM_PC_Rio_B_Equipment (0.83초) [장비]
- **시퀀스 길이**: 1.50초
- **설명**: 마석의 힘을 개방하여 연계 점수 3점을 획득하고, 15초 동안 은신 및 투시 효과를 획득합니다. 대상의 뒤를 공격 시 '약점' 판정이 적용됩니다.
---
## 6. Clad (클라드) - 서포터/힐러
### 기본 정보
- **역할**: 서포터/힐러
- **주 스탯**: CON 20, WIS 20
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: Mace, Heavy, Light
- **평타**: mace 2타
### 평타 상세 정보
**mace** (2타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Clad_Base_Attack_Mace1 | 1.90 | +5.0 | |
| 2 | AM_PC_Clad_Base_Attack_Mace2 | 2.27 | +5.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK150206 치유**
- **타입**: MagicalSkill
- **피해 배율**: 1
- **쿨타임**: 3초 / **마나**: 12 / **시전시간**: 1초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Clad_Base_B_Skill_Ready (2.23초) [준비]
2. AM_PC_Clad_Base_B_Skill_HolyCure (1.17초)
- **시퀀스 길이**: 1.17초
- **설명**: 대상의 체력의 60 회복합니다. 대상이 없을 경우 자신에게 시전합니다.
2. **SK150201 다시 흙으로**
- **타입**: MagicalSkill / **속성**: Holy
- **피해 배율**: 1.5
- **쿨타임**: 5초 / **마나**: 9 / **시전시간**: 0.5초
- **몽타주**:
1. AM_PC_Clad_Base_B_Skill_Ready (2.23초) [준비]
2. AM_PC_Clad_Base_B_Skill_TurnUndead (1.20초)
- **시퀀스 길이**: 1.20초
- **설명**: 3m 내의 적에게 150% 빛 속성 마법 피해를 주고, 3초 동안 5 방어력을 감소 시킵니다.
3. **SK150202 신성한 빛**
- **타입**: MagicalSkill
- **쿨타임**: 7.5초 / **마나**: 15 / **시전시간**: 1초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Clad_Base_B_Skill_Ready (2.23초) [준비]
2. AM_PC_Clad_Base_B_Skill_HolyLight (1.17초)
- **시퀀스 길이**: 1.17초
- **설명**: 주변 아군의 지속 피해 효과를 제거하고. 6초 동안 면역 효과를 제공합니다.
**서브 스킬**:
**SK150101 방패 방어**
- **타입**: Normal
- **피해 배율**: 1
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Clad_Base_B_Skill_Block
- **시퀀스 길이**: 5.30초
- **설명**: 방패로 공격을 방어합니다. 방어 유지 시 지구력이 소모됩니다.
**궁극기**:
**SK150301 마석 ‘황금’**
- **타입**: Normal
- **피해 배율**: 300
- **시전시간**: 0.55초
- **몽타주**:
1. AM_PC_Clad_Base_B_Skill_Gold (1.50초)
2. AM_PC_Clad_B_Equipment (1.00초) [장비]
- **시퀀스 길이**: 1.50초
- **설명**: 마석의 힘을 해방하여 5초 동안 자신과 아군에게 300의 보호막을 생성합니다.
---
## 7. Rene (레네) - 소환사/마법 딜러
### 기본 정보
- **역할**: 소환사/마법 딜러
- **주 스탯**: WIS 25, INT 20
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: Staff, Light, Cloth
- **평타**: staff 3타
### 평타 상세 정보
**staff** (3타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Rene_B_Attack_W01_01 | 1.90 | 0.0 | |
| 2 | AM_PC_Rene_B_Attack_W01_02 | 1.80 | 0.0 | |
| 3 | AM_PC_Rene_B_Attack_W01_03 | 2.20 | 0.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK160202 정령 소환 : 화염**
- **타입**: MagicalSkill
- **피해 배율**: 1.2
- **쿨타임**: 7초 / **마나**: 8
- ⚠️ **Burn 상태이상 유발**: 대상 MaxHP의 10% (3초간)
- 💡 **DoT 피해는 대상 HP에 비례** (구체적 DPS는 다음 챕터 참조)
- 🔮 **소환**: Ifrit (지속 20초)
- **몽타주**: AM_PC_Rene_B_Skill_SummonIfrit
- **시퀀스 길이**: 1.46초
- **설명**: 20초 동안 유지되는 화염의 정령을 소환합니다. 정령은 이동하지 않고 화염 화살을 발사하여 120% 화염 속성 마법 피해를 입힙니다.
2. **SK160206 정령 소환 : 냉기**
- **타입**: MagicalSkill
- **피해 배율**: 0.8
- **쿨타임**: 10초 / **마나**: 15
- 🔮 **소환**: Shiva (지속 60초)
- **몽타주**: AM_PC_Rene_B_Skill_SummonShiva
- **시퀀스 길이**: 2.69초
- **설명**: 60초 동안 유지되는 냉기의 정령을 소환합니다. 정령은 레네를 따라 이동하며 얼음 송곳을 소환합니다. 얼음 송곳은 80%만큼 물 속성 마법 피해를 입히며, 적중된 적은 둔화됩니다.
3. **SK160203 독기 화살**
- **타입**: MagicalSkill / **속성**: Dark
- **피해 배율**: 1
- **쿨타임**: 10초 / **마나**: 15 / **시전시간**: 2초
- ⚠️ **Bleed 상태이상 유발**: 고정 20 피해 (5초간)
- 💡 **DoT 피해는 대상 HP에 비례** (구체적 DPS는 다음 챕터 참조)
- **몽타주**: AM_PC_Rene_B_Skill_PoisonGas
- **시퀀스 길이**: 4.67초
- **설명**: 방어력을 무시하고 30만큼 암흑 속성 마법 피해를 입힙니다. 적중된 적은 출혈 상태가 됩니다.
**서브 스킬**:
**SK160101 할퀴기**
- **타입**: MagicalSkill
- **피해 배율**: 0.75
- **몽타주**:
1. AM_PC_Rene_B_Skill_Scratching (1.11초)
2. AM_PC_Rene_B_Skill_Scratching2 (1.61초)
- **시퀀스 길이**: 1.36초 (평균)
- **설명**: 손톱을 휘둘러 75%만큼 마법 피해를 입히고 흡혈합니다. 피해의 30%만큼 체력을 회복합니다.
**궁극기**:
**SK160301 마석 ‘붉은 축제’**
- **타입**: MagicalSkill
- **피해 배율**: 50
- **시전시간**: 2초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Rene_B_Skill_ManaStoneCarnival (1.50초)
2. AM_PC_Rene_B_Equipment (1.00초) [장비]
- **시퀀스 길이**: 1.50초
- **설명**: 마석의 힘을 해방하여 20초 동안 자신과 아군의 모든 공격에 흡혈 효과를 부여합니다. 피해의 50%만큼 체력을 회복합니다.
### 소환체
#### 🔥 Ifrit
- **소환 스킬**: SK160202 정령 소환 : 화염
- **소환 유지 시간**: 20초
- **공격 몽타주**:
- AM_Sum_Elemental_Fire_Attack_N01 (2.29초)
- AM_Sum_Elemental_Fire_Attack_N02 (2.29초)
- AM_Sum_Elemental_Fire_Attack_N03 (3.70초)
- **공격 사이클**: 2.29초 → 2.29초 → 3.70초 (총 8.29초, 반복)
- **예상 공격 횟수**: ~7.2회
- **총 피해 배율**: ~8.69배 상당
- **특수 효과**: Burn DoT (대상 MaxHP의 10% (3초간))
#### ❄️ Shiva
- **소환 스킬**: SK160206 정령 소환 : 냉기
- **소환 유지 시간**: 60초
- **공격 몽타주**:
- AM_Sum_Elemental_Ice_Attack_N01 (2.32초)
- **공격 사이클**: 2.32초 (반복)
- **예상 공격 횟수**: ~25.9회
- **총 피해 배율**: ~20.72배 상당
---
## 8. Sinobu (시노부) - 기동형 암살자
### 기본 정보
- **역할**: 기동형 암살자
- **주 스탯**: DEX 25, CON 15
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: ShortSword, Cloth, Light
- **평타**: shortSword 2타
### 평타 상세 정보
**shortSword** (2타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Sinobu_B_Attack_W01_03 | 1.07 | -20.0 | |
| 2 | AM_PC_Sinobu_B_Attack_W01_01 | 1.20 | -20.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK180202 기폭찰**
- **타입**: PhysicalSkill
- **피해 배율**: 1.3
- **쿨타임**: 6초 / **마나**: 10 / **시전시간**: 1초
- **몽타주**: AM_PC_Sinobu_B_Skill_BombTalisman
- **시퀀스 길이**: 2.14초
- **설명**: 뒤로 점프하며 기폭찰 쿠나이를 설치합니다. 기폭찰 쿠나이는 적이 근처에 오면 폭발하여 130%만큼 물리 피해를 입힙니다. 사용 시 표창 1개를 충전합니다.
2. **SK180203 비뢰각**
- **타입**: PhysicalSkill / **속성**: Lightning
- **피해 배율**: 1.1
- **쿨타임**: 8초 / **마나**: 11
- **몽타주**:
1. AM_PC_Sinobu_B_Skill_ThunderKick (0.60초)
2. AM_PC_Sinobu_B_Skill_ThunderKick_E (0.87초)
- **시퀀스 길이**: 1.47초
- **설명**: 대각선으로 날아차기를 하여 110%만큼 번개 속성 물리 피해를 입힙니다. 점프 상태에서만 사용 가능하며, 적중된 대상은 잠시 경직됩니다. 적중 시 표창 1개를 충전합니다.
3. **SK180205 인술 ‘바꿔치기’**
- **타입**: PhysicalSkill
- **피해 배율**: 0.9
- **쿨타임**: 11초 / **마나**: 12 / **시전시간**: 1.5초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Sinobu_B_Skill_NinpoChange (3.57초)
2. AM_PC_Sinobu_B_Equipment (0.83초) [장비]
3. AM_PC_Sinobu_B_Skill_NinpoChange_Active (1.00초)
- **시퀀스 길이**: 4.57초
- **설명**: 7초 동안 유지되는 '바꿔치기'를 사용합니다. '바꿔치기' 상태 중 피격 시 피해가 50% 감소하며, 3초 동안 투명화와 이동속도 증가 효과를 얻습니다. 효과 발동 시 표창 1개를 충전합니다.
**서브 스킬**:
**SK180101 표창**
- **타입**: PhysicalSkill
- **피해 배율**: 1.2
- **시전시간**: 1초
- **몽타주**: AM_PC_Sinobu_B_Skill_Shuriken
- **시퀀스 길이**: 0.88초
- **설명**: 표창을 던져 120%만큼 물리 피해를 입힙니다. 일반 공격이 적중하거나 스킬을 사용하면 충전됩니다. 최대 3개까지 충전 가능합니다.
**궁극기**:
**SK180301 마석 '반환'**
- **타입**: Normal
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Sinobu_Base_Skill_Deflect
- **시퀀스 길이**: 2.33초
- **설명**: 마석의 힘을 개방하여 7초 동안 전방의 투사체 공격을 튕겨내고 근접 공격을 막아냅니다.
---
## 9. Lian (리옌) - 정밀 원거리 딜러
### 기본 정보
- **역할**: 정밀 원거리 딜러
- **주 스탯**: DEX 20, WIS 20
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: Bow, Light, Cloth
- **평타**: bow 1타
### 평타 상세 정보
**bow** (1타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Lian_Base_000_Attack_Bow | 3.27 | 0.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK190207 속사**
- **타입**: PhysicalSkill
- **피해 배율**: 0.85
- **쿨타임**: 7초 / **마나**: 16
- **몽타주**: AM_PC_Lian_Base_000_Skill_RapidShot1
- **시퀀스 길이**: 2.67초
- **설명**: 4발의 화살을 빠르게 발사하여 각각 85%만큼 물리 피해를 입힙니다. 화살을 4개 소모합니다.
2. **SK190205 비연사**
- **타입**: PhysicalSkill
- **피해 배율**: 1.5
- **쿨타임**: 10초 / **마나**: 15
- **몽타주**:
1. AM_PC_Lian_Base_000_Skill_BackStepBowAttack (1.33초)
2. AM_PC_Lian_Base_000_Skill_BackStepBowAttack (1.33초)
- **시퀀스 길이**: 1.33초
- **설명**: 뒤로 빠지며 화살을 발사하여 150%만큼 물리 피해를 입힙니다. 화살을 1개 소모합니다.
3. **SK190201 연화**
- **타입**: PhysicalSkill / **속성**: Holy
- **피해 배율**: 1.2
- **쿨타임**: 7.5초 / **마나**: 12
- **몽타주**: AM_PC_Lian_Base_000_Skill_DarkSouls_NoCasting
- **시퀀스 길이**: 2.20초
- **설명**: 60초 동안 적을 천천히 추적하는 연꽃을 만들어 발사합니다. 연꽃은 120%만큼 빛 속성 물리 피해를 입히며, 적중된 대상은 10초 동안 25%의 주는 피해 감소 효과를 받습니다.
4. **SK190209 재장전**
- **타입**: Normal
- **피해 배율**: 1
- **시전시간**: 5초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Lian_Base_000_Skill_Reload (1.43초)
2. AM_PC_Lian_Base_000_Interaction_Equipment (1.00초) [장비]
- **시퀀스 길이**: 1.43초
- **설명**: 화살을 화살통에 장전 합니다.
**서브 스킬**:
**SK190101 정조준**
- **타입**: PhysicalSkill
- **피해 배율**: 0.7
- **시전시간**: 1.5초
- **몽타주**: AM_PC_Lian_Base_000_Skill_ChargingBow
- **시퀀스 길이**: 4.93초
- **설명**: 조준하는 동안 물리 피해가 증가하는 화살을 발사합니다. 최대 150%까지 물리 피해가 증가합니다. 화살을 1개 소모합니다.
**궁극기**:
**SK190301 마석 '폭우'**
- **타입**: PhysicalSkill
- **피해 배율**: 50
- **시전시간**: 1.5초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**:
1. AM_PC_Lian_Base_000_Skill_ManastoneSilence (1.50초)
2. AM_PC_Lian_Base_000_Interaction_Equipment (1.00초) [장비]
- **시퀀스 길이**: 1.50초
- **설명**: 마석의 힘을 개방하여 15초 동안 화살을 소모하지 않으며, 쿨타임이 50% 감소합니다.
---
## 10. Cazimord (카지모르드) - 고숙련도 하이브리드 전사
### 기본 정보
- **역할**: 고숙련도 하이브리드 전사
- **주 스탯**: DEX 25, STR 15
- **HP**: 100 | **MP**: 50 | **마나 회복**: 0.2/초
- **크리티컬**: 확률 5% | 추가 피해 0%
- **장착 가능**: WeaponShield, Light, Cloth
- **평타**: weaponShield 3타
### 평타 상세 정보
**weaponShield** (3타 콤보):
| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |
|------|--------|----------|---------|------|
| 1 | AM_PC_Cazimord_B_Attack_W01_01 | 1.67 | -15.0 | |
| 2 | AM_PC_Cazimord_B_Attack_W01_02 | 1.90 | +5.0 | |
| 3 | AM_PC_Cazimord_B_Attack_W01_03 | 1.87 | +10.0 | |
### 스킬 목록
**기본 스킬**:
1. **SK170201 섬광**
- **타입**: PhysicalSkill
- **피해 배율**: 0.5
- **쿨타임**: 15.5초 / **마나**: 5
- ⚠️ **Burn 상태이상 유발**: 대상 MaxHP의 10% (3초간)
- 💡 **DoT 피해는 대상 HP에 비례** (구체적 DPS는 다음 챕터 참조)
- **몽타주**:
1. AM_PC_Cazimord_B_Skill_Flash (3.62초)
2. AM_PC_Cazimord_B_Skill_Flash_Active (1.73초)
- **시퀀스 길이**: 1.73초
- **설명**: 정면으로 4m 돌진하며, 베기 공격으로 공격력의 100%의 피해를 가합니다. 스킬 사용 중에는 경직에 면역 됩니다.
2. **SK170202 날개 베기**
- **타입**: PhysicalSkill
- **피해 배율**: 0.3
- **쿨타임**: 15.5초 / **마나**: 10
- **몽타주**: AM_PC_Cazimord_B_Skill_BladeStorm
- **시퀀스 길이**: 2.00초
- **설명**: 4번 베기로 공격력의 30% 피해를 입힙니다. 스킬 사용 중에는 경직에 면역 됩니다.
3. **SK170203 작열**
- **타입**: Normal
- **피해 배율**: 1
- **쿨타임**: 27.5초 / **마나**: 3 / **시전시간**: 2초
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Cazimord_B_Skill_Burn
- **시퀀스 길이**: 2.43초
- **설명**: 15초간 무기에 불을 붙여서 적중시킨 적에게 물리 피해의 20%만큼을 추가 마법 피해로 주고, 화상 상태로 만듭니다.
**서브 스킬**:
**SK170101 흘리기**
- **타입**: PhysicalSkill
- **피해 배율**: 1
- 💡 **유틸리티 스킬** (DPS 계산 제외)
- **몽타주**: AM_PC_Cazimord_B_Skill_Parrying
- **시퀀스 길이**: 1.61초
- **설명**: 무기로 적의 공격을 흘려냅니다. 흘리기에 성공하면 적의 공격을 막아냅니다. 그리고 스킬의 재사용 대기시간 일부가 감소됩니다. 섬광 및 날개베기는 각각 3.8초, 작열은 6.8초씩 감소 합니다.
**궁극기**:
**SK170301 마석 '칼날폭풍'**
- **타입**: PhysicalSkill
- **피해 배율**: 0.8
- **시전시간**: 2초
- **몽타주**: AM_PC_Cazimord_B_Skill_ManaStoneBurn
- **시퀀스 길이**: 3.50초
- **설명**: 마석의 힘을 빌려 빠르게 정면을 12회 공격해 각각 80%의 물리 피해를 입힙니다. 마지막 2회의 타격은 100%의 물리 피해를 입힙니다. 시전 중에는 천천히 이동할 수 있지만, 마지막 타격때는 이동할 수 없습니다.
---
---
**생성 일시**: 2025-10-27 16:45:41
**데이터 소스**: validated_data.json
**검증 상태**: 검증 완료 ✅

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
# 데이터 검증 리포트
**생성 시각**: 1761546661.7020578
## 전체 요약
- ✅ 검증 통과: **109개** 항목
- ⚠️ 경고: **0개** 항목
- ❌ 실패: **0개** 항목
- 📊 데이터 신뢰도: **100.0%**
## ✅ 통과 항목
총 109개 항목이 검증을 통과했습니다.

Binary file not shown.

View File

@ -0,0 +1,122 @@
# 아카이브된 분석 스크립트
이 디렉토리에는 개발 과정에서 사용된 일회성 체크 및 검증 스크립트들이 보관되어 있습니다.
**아카이브 일자**: 2025-10-27
**사유**: 핵심 분석 파이프라인 완성 후 정리
---
## 📁 파일 분류
### 🔍 스킬 검증 스크립트
특정 스킬의 데이터 추출 및 노티파이 검증에 사용된 스크립트
- **check_baran_clad_skills.py** - 바란/클라드 스킬 검증 (SK130301, SK150201)
- **check_lian_skills.py** - 리안 스킬 검증 1차
- **check_lian_skills2.py** - 리안 스킬 검증 2차
- **check_sk150201.py** - 클라드 SK150201 상세 분석
### 🏗️ 데이터 구조 탐색 스크립트
JSON 파일 구조 및 Blueprint 데이터 탐색
- **check_json_structure.py** - JSON 최상위 구조 확인
- **check_first_asset.py** - 첫 번째 Asset 구조 출력
- **check_data.py** - 전반적인 데이터 구조 확인
- **check_skill_structure.py** - DT_Skill 구조 분석
### 🎯 Character Ability 탐색 스크립트
DT_CharacterAbility 및 평타 몽타주 추출 검증
- **check_character_ability.py** - DT_CharacterAbility 기본 구조 확인
- **check_character_ability2.py** - attackMontageMap 추출 검증
- **check_character_ability3.py** - 평타 몽타주 상세 분석
### 🎬 AnimMontage 및 Notify 분석
AnimNotify 및 투사체 판정 로직 검증
- **check_montage_names.py** - 몽타주 이름 추출 검증
- **check_send_event_notify.py** - SimpleSendEvent 노티파이 분석
- **investigate_projectile.py** - 투사체 노티파이 상세 조사
### 🧪 Blueprint 변수 검증
Blueprint 변수 추출 및 매칭 검증
- **check_bp_vars.py** - Blueprint 변수 기본 추출
- **check_bp_verification.py** - Blueprint 변수 검증 로직
### ✅ 개선 사항 검증
버전별 개선 사항 적용 여부 확인
- **check_improvements.py** - v2.1~v2.2 개선사항 검증
- **verify_improvements.py** - 일반 개선사항 검증
- **verify_improvements_v2.3.py** - v2.3 개선사항 검증
---
## 📝 사용 목적
이 스크립트들은 다음 목적으로 작성되었습니다:
1. **데이터 구조 탐색**: JSON 및 Blueprint 데이터 구조 이해
2. **추출 로직 검증**: 몽타주, 노티파이, 스킬 데이터 추출 정확성 확인
3. **버그 수정**: 특정 스킬의 오류 원인 분석 및 해결
4. **개선사항 검증**: 버전 업데이트 후 변경사항 적용 확인
---
## 🔄 재사용 가능성
### 재사용 가능한 스크립트
다음 스크립트들은 향후 유사한 문제 발생 시 참고 가능합니다:
- **check_send_event_notify.py** - SimpleSendEvent 노티파이 분석 템플릿
- **investigate_projectile.py** - 투사체 노티파이 조사 방법
- **check_bp_vars.py** - Blueprint 변수 추출 예시
### 재사용 방법
```bash
# 예: 새로운 스킬 SK999999 분석이 필요한 경우
# check_sk150201.py를 복사하여 수정
cd D:\Work\WorldStalker\DS-전투분석_저장소\분석도구\v2\archive
cp check_sk150201.py check_sk999999.py
# 내부의 스킬 ID를 SK999999로 변경 후 실행
python check_sk999999.py
```
---
## 🗑️ 삭제 가능 여부
이 스크립트들은 현재 분석 파이프라인에서 사용되지 않지만, 다음 이유로 보존합니다:
1. **디버깅 참고**: 향후 유사한 문제 발생 시 해결 방법 참고
2. **데이터 구조 이해**: 새로운 개발자가 JSON 구조를 이해하는 데 도움
3. **분석 히스토리**: 시스템 개발 과정 기록
**권장 보존 기간**: 6개월~1년
만약 디스크 공간이 부족하거나 더 이상 필요 없다고 판단되면 삭제해도 무방합니다.
---
## 📚 관련 문서
- **../장기과제_Blueprint변수검증.md** - Blueprint 변수 활용 계획
- **../../분석결과/*/개선_보고서_*.md** - 버전별 개선 내역
- **../../ARCHITECTURE.md** - 전체 시스템 아키텍처
---
**작성자**: AI-assisted Development Team
**최종 업데이트**: 2025-10-27

View File

@ -0,0 +1,81 @@
"""바란, 클라드 스킬 몽타주 확인"""
import json
with open('../../원본데이터/AnimMontage.json', 'r', encoding='utf-8') as f:
montage_data = json.load(f)
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
dt_data = json.load(f)
# DT_Skill 찾기
dt_skill = None
for asset in dt_data.get('Assets', []):
if asset.get('AssetName') == 'DT_Skill':
dt_skill = asset
break
print("=== 바란, 클라드 스킬 몽타주 확인 ===\n")
target_skills = {
'SK130301': '일격분쇄 (바란)',
'SK150201': '다시 흙으로 (클라드)'
}
skill_montages = {}
for row in dt_skill.get('Rows', []):
row_name = row.get('RowName', '')
if row_name in target_skills:
row_data = row.get('Data', {})
use_montages = row_data.get('useMontages', [])
skill_name = row_data.get('name', '')
print(f"[{row_name}] {skill_name}")
print(f" useMontages: {len(use_montages)}")
if use_montages:
for montage_path in use_montages:
print(f" Path: {montage_path}")
# 몽타주 이름 추출
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
if row_name not in skill_montages:
skill_montages[row_name] = []
skill_montages[row_name].append(montage_name)
print(f" Name: {montage_name}")
print()
# 각 몽타주에서 노티파이 확인
print("\n=== 몽타주 노티파이 확인 ===\n")
for skill_id, montage_names in skill_montages.items():
for montage_name in montage_names:
for asset in montage_data.get('Assets', []):
if asset.get('AssetName') == montage_name:
print(f"[{skill_id}] {target_skills[skill_id]} - {montage_name}")
notifies = asset.get('AnimNotifies', [])
print(f" 총 노티파이: {len(notifies)}\n")
found_attack_notify = False
for idx, notify in enumerate(notifies):
notify_class = notify.get('NotifyClass', '')
# SimpleSendEvent 노티파이
if 'SimpleSendEvent' in notify_class:
custom_props = notify.get('CustomProperties', {})
event_tag = custom_props.get('Event Tag', '')
# Event.SkillActivate 확인
if 'SkillActivate' in event_tag:
print(f" [{idx}] SimpleSendEvent")
print(f" Event Tag: {event_tag}")
print(f" >>> Event.SkillActivate 발견! (공격 스킬)")
found_attack_notify = True
print()
if not found_attack_notify:
print(" *** Event.SkillActivate를 찾지 못했습니다. ***\n")
print("-" * 60)
print()

View File

@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Blueprint 변수 상세 조사"""
import json
from pathlib import Path
# Blueprint.json 로드
bp_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/원본데이터/Blueprint.json")
with open(bp_file, 'r', encoding='utf-8') as f:
bp_data = json.load(f)
# DataTable.json 로드
dt_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/원본데이터/DataTable.json")
with open(dt_file, 'r', encoding='utf-8') as f:
dt_data = json.load(f)
assets = bp_data.get('Assets', [])
# GA_Skill_Knight_Counter 찾기
counter_bp = [a for a in assets if a.get('AssetName') == 'GA_Skill_Knight_Counter']
print("=" * 80)
print("GA_Skill_Knight_Counter Blueprint 분석")
print("=" * 80)
if counter_bp:
bp = counter_bp[0]
print(f"\nAssetName: {bp.get('AssetName')}")
print(f"ParentClass: {bp.get('ParentClass')}")
vars = bp.get('Variables', [])
print(f"\nTotal Variables: {len(vars)}")
# 숫자 타입 변수만
numeric_types = ['Float', 'Int', 'Double', 'Byte', 'int', 'float', 'double']
numeric_vars = []
for v in vars:
var_type = str(v.get('VarType', ''))
if any(t in var_type for t in numeric_types):
numeric_vars.append(v)
print(f"\nNumeric Variables ({len(numeric_vars)}개):")
for v in numeric_vars[:15]:
print(f" {v.get('VarName')}: {v.get('DefaultValue')} (type: {v.get('VarType')})")
# 모든 변수 출력 (참고용)
print(f"\nAll Variables:")
for v in vars[:20]:
print(f" {v.get('VarName')} ({v.get('VarType')}): {v.get('DefaultValue')}")
else:
print("GA_Skill_Knight_Counter를 찾을 수 없음!")
# DT_Skill에서 SK100202 정보
print("\n" + "=" * 80)
print("SK100202 DT_Skill 데이터")
print("=" * 80)
dt_assets = dt_data.get('Assets', [])
dt_skill = [a for a in dt_assets if a.get('AssetName') == 'DT_Skill'][0]
rows = dt_skill.get('Rows', [])
sk_row = [r for r in rows if r.get('RowName') == 'SK100202'][0]
sk_data = sk_row.get('Data', {})
print(f"스킬 이름: {sk_data.get('name')}")
print(f"Desc: {sk_data.get('desc')}")
print(f"DescValues: {sk_data.get('descValues')}")
# desc에서 {0}, {1} 위치 찾기
desc = sk_data.get('desc', '')
desc_values = sk_data.get('descValues', [])
print(f"\n변수 매칭:")
for i, value in enumerate(desc_values):
print(f" {{{i}}} = {value}")
print(f"\n추론: Hilda 반격 스킬은")
print(f" - {{{0}}}초 = {desc_values[0]}초 (반격 지속 시간)")
print(f" - {{{1}}}% = {desc_values[1]}% (반격 피해 배율)")
# 다른 스킬도 조사
print("\n" + "=" * 80)
print("다른 스킬 Blueprint 변수 조사")
print("=" * 80)
test_skills = [
('SK100201', 'GA_Skill_Hilda_SwordStrike', '칼날 격돌'),
('SK110205', 'GA_Skill_Urud_MultiShot_Quick', '다발 화살'),
]
for skill_id, bp_name, skill_name in test_skills:
print(f"\n=== {skill_name} ({skill_id}) ===")
# DT_Skill
sk_row = [r for r in rows if r.get('RowName') == skill_id]
if sk_row:
sk_data = sk_row[0].get('Data', {})
print(f"DescValues: {sk_data.get('descValues')}")
# Blueprint
bp_match = [a for a in assets if a.get('AssetName') == bp_name]
if bp_match:
vars = bp_match[0].get('Variables', [])
numeric_vars = [v for v in vars if any(t in str(v.get('VarType', '')) for t in numeric_types)]
print(f"Blueprint 변수 총 {len(vars)}개, 숫자형 {len(numeric_vars)}")
if numeric_vars:
print(" 숫자형 변수:")
for v in numeric_vars[:5]:
print(f" {v.get('VarName')}: {v.get('DefaultValue')}")
else:
print(f" Blueprint '{bp_name}' 없음")

View File

@ -0,0 +1,120 @@
#!/usr/bin/env python3
"""Blueprint 변수 검증 조사 스크립트"""
import json
import sys
from pathlib import Path
# Blueprint.json 로드
bp_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/원본데이터/Blueprint.json")
with open(bp_file, 'r', encoding='utf-8') as f:
bp_data = json.load(f)
# DataTable.json 로드
dt_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/원본데이터/DataTable.json")
with open(dt_file, 'r', encoding='utf-8') as f:
dt_data = json.load(f)
# validated_data.json 로드
val_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/분석결과/20251024_210822_v2/validated_data.json")
with open(val_file, 'r', encoding='utf-8') as f:
val_data = json.load(f)
print("=" * 80)
print("Blueprint 변수 검증 조사")
print("=" * 80)
# 예시: Hilda의 SK100202 (반격) 스킬 조사
print("\n=== 예시: Hilda SK100202 (반격) 스킬 ===")
# DT_Skill에서 SK100202 정보
assets = dt_data.get('Assets', [])
dt_skill = [a for a in assets if a.get('AssetName') == 'DT_Skill'][0]
rows = dt_skill.get('Rows', [])
sk100202_row = [r for r in rows if r.get('RowName') == 'SK100202'][0]
sk100202_data = sk100202_row.get('Data', {})
print(f"스킬 이름: {sk100202_data.get('name')}")
print(f"Desc: {sk100202_data.get('desc')}")
print(f"DescValues: {sk100202_data.get('descValues')}")
print(f"AbilityClass: {sk100202_data.get('abilityClass')}")
# Blueprint에서 관련 GA_Skill 찾기
ability_class = sk100202_data.get('abilityClass', '')
if ability_class:
bp_name = ability_class.split('/')[-1].split('.')[0] if '/' in ability_class else ability_class
print(f"\nBlueprint 이름: {bp_name}")
# Blueprint.json에서 검색
bp_assets = bp_data.get('Assets', [])
matching_bp = [a for a in bp_assets if a.get('AssetName') == bp_name]
if matching_bp:
print(f"Blueprint 발견: {matching_bp[0].get('AssetName')}")
# 변수 확인
variables = matching_bp[0].get('BlueprintVariables', [])
print(f"\nBlueprint 변수 ({len(variables)}개):")
for var in variables[:10]: # 처음 10개만
var_name = var.get('VarName', 'N/A')
var_type = var.get('VarType', 'N/A')
default_value = var.get('DefaultValue', 'N/A')
print(f" - {var_name} ({var_type}): {default_value}")
else:
print(f"Blueprint '{bp_name}' 찾을 수 없음")
# validated_data에서 확인
hilda = val_data['hilda']
sk = hilda['skills']['SK100202']
print(f"\n=== Validated Data ===")
print(f"DescFormatted: {sk.get('descFormatted')}")
print(f"Blueprint Variables in extracted data:")
bp_vars = sk.get('blueprintVariables', {})
if bp_vars:
for var_name, var_info in list(bp_vars.items())[:5]:
print(f" - {var_name}: {var_info}")
else:
print(" (Blueprint 변수 없음)")
# 다른 스킬도 몇 개 조사
print("\n" + "=" * 80)
print("다른 스킬 샘플 조사")
print("=" * 80)
sample_skills = [
('SK100201', 'hilda', '칼날 격돌'),
('SK110205', 'urud', '다발 화살'),
('SK160202', 'rene', 'Ifrit 소환')
]
for skill_id, stalker, skill_name in sample_skills:
print(f"\n=== {skill_id} ({skill_name}) ===")
# DT_Skill
skill_row = [r for r in rows if r.get('RowName') == skill_id]
if not skill_row:
print(" DT_Skill에 없음")
continue
skill_data = skill_row[0].get('Data', {})
desc = skill_data.get('desc', '')
desc_values = skill_data.get('descValues', [])
ability_class = skill_data.get('abilityClass', '')
print(f" Desc: {desc[:100]}...")
print(f" DescValues: {desc_values}")
print(f" AbilityClass: {ability_class}")
# Blueprint
if ability_class:
bp_name = ability_class.split('/')[-1].split('.')[0] if '/' in ability_class else ability_class
matching_bp = [a for a in bp_assets if a.get('AssetName') == bp_name]
if matching_bp:
variables = matching_bp[0].get('BlueprintVariables', [])
print(f" Blueprint 변수 개수: {len(variables)}")
# 숫자 타입 변수 찾기
numeric_vars = [v for v in variables if any(t in str(v.get('VarType', '')) for t in ['Float', 'Int', 'Byte'])]
if numeric_vars:
print(f" 숫자 변수 샘플:")
for var in numeric_vars[:3]:
print(f" - {var.get('VarName')}: {var.get('DefaultValue')}")

View File

@ -0,0 +1,69 @@
"""DT_CharacterAbility 데이터 구조 확인"""
import json
import sys
def check_character_ability():
# DataTable.json 로드
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
# DT_CharacterAbility 찾기
dt_char = None
for table in data:
if table.get('Type') == 'DataTable' and 'CharacterAbility' in table.get('Name', ''):
dt_char = table
break
if not dt_char:
print("❌ DT_CharacterAbility 테이블을 찾을 수 없습니다.")
return
print(f"✅ DT_CharacterAbility 테이블 발견: {dt_char.get('Name')}")
print(f" Rows: {len(dt_char.get('Rows', {}))}")
# 우르드 데이터 확인
rows = dt_char.get('Rows', {})
urud_data = None
for row_name, row_data in rows.items():
if 'urud' in row_name.lower() or 'Urud' in row_name:
urud_data = row_data
print(f"\n🔍 우르드 데이터 발견: {row_name}")
break
if urud_data:
print("\n📋 우르드 데이터 키:")
for key in sorted(urud_data.keys()):
print(f" - {key}")
# Attack Montage Map 확인
if 'attackMontageMap' in urud_data:
print(f"\n⚔️ Attack Montage Map:")
attack_map = urud_data['attackMontageMap']
print(f" 타입: {type(attack_map)}")
if isinstance(attack_map, dict):
for k, v in attack_map.items():
print(f" - {k}: {v}")
elif isinstance(attack_map, list):
for idx, item in enumerate(attack_map):
print(f" [{idx}]: {item}")
else:
print(f" 값: {attack_map}")
# 리옌 데이터 확인
lian_data = None
for row_name, row_data in rows.items():
if 'lian' in row_name.lower() or 'Lian' in row_name:
lian_data = row_data
print(f"\n🔍 리옌 데이터 발견: {row_name}")
break
if lian_data and 'attackMontageMap' in lian_data:
print(f"\n⚔️ 리옌 Attack Montage Map:")
attack_map = lian_data['attackMontageMap']
print(f" 타입: {type(attack_map)}")
if isinstance(attack_map, dict):
for k, v in attack_map.items():
print(f" - {k}: {v}")
if __name__ == '__main__':
check_character_ability()

View File

@ -0,0 +1,62 @@
"""DT_CharacterAbility 데이터 구조 확인 (수정)"""
import json
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
# Assets에서 CharacterAbility 찾기
assets = data.get('Assets', [])
print(f"총 Assets: {len(assets)}")
# DT_CharacterAbility 찾기
dt_char = None
for asset in assets:
asset_type = asset.get('Type', '')
asset_name = asset.get('Name', '')
if asset_type == 'DataTable' and 'CharacterAbility' in asset_name:
dt_char = asset
print(f"\n발견: {asset_name}")
break
if not dt_char:
print("DT_CharacterAbility를 찾을 수 없습니다.")
exit(1)
# Rows 확인
rows = dt_char.get('Rows', {})
print(f" Rows 개수: {len(rows)}")
print(f" Row 키: {list(rows.keys())}")
# 우르드 확인
for row_name, row_data in rows.items():
if 'Urud' in row_name:
print(f"\n우르드: {row_name}")
print(f" 데이터 키: {list(row_data.keys())}")
# attackMontageMap 확인
if 'attackMontageMap' in row_data:
print(f"\n attackMontageMap:")
attack_map = row_data['attackMontageMap']
print(f" 타입: {type(attack_map)}")
if isinstance(attack_map, dict):
for k, v in attack_map.items():
print(f" [{k}]: {v}")
elif isinstance(attack_map, list):
for idx, item in enumerate(attack_map):
print(f" [{idx}]: {item}")
# 리옌 확인
for row_name, row_data in rows.items():
if 'Lian' in row_name:
print(f"\n리옌: {row_name}")
if 'attackMontageMap' in row_data:
print(f"\n attackMontageMap:")
attack_map = row_data['attackMontageMap']
print(f" 타입: {type(attack_map)}")
if isinstance(attack_map, dict):
for k, v in attack_map.items():
print(f" [{k}]: {v}")

View File

@ -0,0 +1,72 @@
"""DT_CharacterAbility 데이터 구조 확인 (수정3)"""
import json
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
print(f"총 Assets: {len(assets)}\n")
# DT_CharacterAbility 찾기
dt_char = None
for asset in assets:
if asset.get('AssetName') == 'DT_CharacterAbility':
dt_char = asset
print(f"발견: {asset.get('AssetName')}")
print(f" AssetPath: {asset.get('AssetPath')}")
print(f" RowStructure: {asset.get('RowStructure')}")
break
if not dt_char:
print("DT_CharacterAbility를 찾을 수 없습니다.")
exit(1)
# Rows 확인
rows = dt_char.get('Rows', [])
print(f" Rows 개수: {len(rows)}\n")
# 우르드 찾기
print("=== 우르드 데이터 ===")
for row in rows:
row_name = row.get('RowName', '')
if 'urud' in row_name.lower():
print(f"RowName: {row_name}")
row_data = row.get('Data', {})
print(f"Data 키: {list(row_data.keys())}\n")
# attackMontageMap 확인
if 'attackMontageMap' in row_data:
attack_map = row_data['attackMontageMap']
print(f"attackMontageMap 타입: {type(attack_map)}")
if isinstance(attack_map, dict):
print("attackMontageMap 내용 (dict):")
for k, v in attack_map.items():
print(f" [{k}]: {v}")
elif isinstance(attack_map, list):
print(f"attackMontageMap 내용 (list, {len(attack_map)}개):")
for idx, item in enumerate(attack_map):
print(f" [{idx}]: {item}")
else:
print(f"attackMontageMap 값: {attack_map}")
# 리옌 찾기
print("\n=== 리옌 데이터 ===")
for row in rows:
row_name = row.get('RowName', '')
if 'lian' in row_name.lower():
print(f"RowName: {row_name}")
row_data = row.get('Data', {})
if 'attackMontageMap' in row_data:
attack_map = row_data['attackMontageMap']
print(f"attackMontageMap 타입: {type(attack_map)}")
if isinstance(attack_map, dict):
print("attackMontageMap 내용 (dict):")
for k, v in attack_map.items():
print(f" [{k}]: {v}")
elif isinstance(attack_map, list):
print(f"attackMontageMap 내용 (list, {len(attack_map)}개):")
for idx, item in enumerate(attack_map):
print(f" [{idx}]: {item}")

View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""임시 데이터 확인 스크립트"""
import json
from pathlib import Path
# DT_Skill 확인
data_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/원본데이터/DataTable.json")
with open(data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
skill_dt = [a for a in assets if a.get('AssetName') == 'DT_Skill']
if skill_dt:
rows = skill_dt[0].get('Rows', [])
sample = rows[0]
skill_data = sample.get('Data', {})
print(f"RowName: {sample.get('RowName')}")
print(f"Data keys: {list(skill_data.keys())[:20]}")
print(f"\nHas desc: {'desc' in skill_data}")
print(f"Has descValues: {'descValues' in skill_data}")
if 'desc' in skill_data:
print(f"\nDesc sample: {skill_data['desc'][:200]}")
if 'descValues' in skill_data:
print(f"DescValues: {skill_data['descValues']}")
# Check for SK100202 (Hilda counter skill)
hilda_counter = [row for row in rows if row.get('RowName') == 'SK100202']
if hilda_counter:
counter_data = hilda_counter[0].get('Data', {})
print(f"\n\n=== SK100202 (Hilda Counter) ===")
print(f"Desc: {counter_data.get('desc', 'N/A')}")
print(f"DescValues: {counter_data.get('descValues', [])}")
# Check stalker names
char_stat_dt = [a for a in assets if a.get('AssetName') == 'DT_CharacterStat']
if char_stat_dt:
rows = char_stat_dt[0].get('Rows', [])
hilda_row = [row for row in rows if row.get('RowName') == 'hilda']
if hilda_row:
hilda_data = hilda_row[0].get('Data', {})
print(f"\n\n=== Hilda Character Data ===")
print(f"Name: {hilda_data.get('name', 'N/A')}")
print(f"JobName: {hilda_data.get('jobName', 'N/A')}")
print(f"Data keys: {list(hilda_data.keys())[:15]}")

View File

@ -0,0 +1,38 @@
"""첫 번째 Asset 구조 확인"""
import json
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
print(f"총 Assets: {len(assets)}\n")
if assets:
first = assets[0]
print("첫 번째 Asset의 구조:")
print(f" 타입: {type(first)}\n")
if isinstance(first, dict):
print(" 키 목록:")
for key in sorted(first.keys()):
value = first[key]
if isinstance(value, (list, dict)):
print(f" - {key}: {type(value).__name__} (len={len(value)})")
else:
print(f" - {key}: {value}")
# CharacterAbility 관련 찾기
print("\n\nCharacterAbility 관련 Asset 검색:")
for idx, asset in enumerate(assets):
if isinstance(asset, dict):
# 모든 값에서 'CharacterAbility' 문자열 찾기
asset_str = str(asset)
if 'CharacterAbility' in asset_str:
print(f"\n[{idx}] CharacterAbility 발견!")
print(f" 키: {list(asset.keys())[:10]}")
# Name 키가 있는지 확인
if 'Name' in asset:
print(f" Name: {asset['Name']}")
# 첫 10글자만 출력
print(f" 내용 샘플: {asset_str[:200]}...")
break

View File

@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""개선 요청사항 확인 스크립트"""
import json
from pathlib import Path
val_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/분석결과/20251024_213233_v2/validated_data.json")
with open(val_file, 'r', encoding='utf-8') as f:
data = json.load(f)
print("=" * 80)
print("개선 요청사항 확인")
print("=" * 80)
# 1. CastingTime 확인
print("\n1. CastingTime 수집 현황")
print("-" * 80)
casting_skills = []
for stalker_id, stalker_data in data.items():
skills = stalker_data.get('skills', {})
for skill_id, skill in skills.items():
casting_time = skill.get('castingTime', 0)
if casting_time > 0:
casting_skills.append({
'stalker': stalker_id,
'skillId': skill_id,
'name': skill.get('name'),
'castingTime': casting_time
})
print(f"시전시간이 있는 스킬: {len(casting_skills)}")
for item in casting_skills[:10]:
print(f" [{item['stalker']}] {item['skillId']} - {item['name']}: {item['castingTime']}")
# 2. SK170101 소수점 문제 확인
print("\n2. SK170101 (카지모르드 흘리기) 소수점 문제")
print("-" * 80)
cazi = data['cazimord']
sk170101 = cazi['skills']['SK170101']
print(f"DescValues (원본): {sk170101.get('descValues')}")
print(f"DescFormatted: {sk170101.get('descFormatted')[:100]}...")
# 3. Projectile Shot TriggerTime 확인
print("\n3. Projectile Shot 노티파이 TriggerTime")
print("-" * 80)
projectile_skills = []
for stalker_id, stalker_data in data.items():
skills = stalker_data.get('skills', {})
for skill_id, skill in skills.items():
montages = skill.get('montageData', [])
for montage in montages:
all_notifies = montage.get('allNotifies', [])
for notify in all_notifies:
notify_class = notify.get('NotifyClass', '')
if 'Trigger_Projectile_Shot' in notify_class:
projectile_skills.append({
'stalker': stalker_id,
'skillId': skill_id,
'name': skill.get('name'),
'montage': montage.get('assetName'),
'triggerTime': notify.get('TriggerTime', 0),
'sequenceLength': montage.get('sequenceLength', 0),
'actualDuration': montage.get('actualDuration', 0)
})
print(f"Projectile Shot 노티파이가 있는 스킬: {len(projectile_skills)}")
for item in projectile_skills[:10]:
print(f" [{item['stalker']}] {item['skillId']} - {item['name']}")
print(f" Montage: {item['montage']}")
print(f" TriggerTime: {item['triggerTime']:.3f}초 (전체: {item['actualDuration']:.2f}초)")
print(f" 빠른 발사: {item['actualDuration'] - item['triggerTime']:.3f}초 단축 가능")
# 4. DoT 스킬 확인
print("\n4. DoT 스킬 현황")
print("-" * 80)
dot_skills = []
for stalker_id, stalker_data in data.items():
skills = stalker_data.get('skills', {})
for skill_id, skill in skills.items():
is_dot = skill.get('isDot', False)
if is_dot:
dot_skills.append({
'stalker': stalker_id,
'skillId': skill_id,
'name': skill.get('name'),
'damageRate': skill.get('skillDamageRate', 0)
})
print(f"DoT 스킬: {len(dot_skills)}")
for item in dot_skills:
print(f" [{item['stalker']}] {item['skillId']} - {item['name']}: rate={item['damageRate']}")
# 5. descValues 소수점 문제가 있는 스킬 찾기
print("\n5. descValues 소수점 문제 스킬 찾기")
print("-" * 80)
long_decimal_skills = []
for stalker_id, stalker_data in data.items():
skills = stalker_data.get('skills', {})
for skill_id, skill in skills.items():
desc_values = skill.get('descValues', [])
for val in desc_values:
if isinstance(val, float):
# 소수점 자리수가 2보다 크면
val_str = str(val)
if '.' in val_str:
decimal_part = val_str.split('.')[1]
if len(decimal_part) > 2:
long_decimal_skills.append({
'stalker': stalker_id,
'skillId': skill_id,
'name': skill.get('name'),
'value': val,
'rounded': round(val, 2)
})
break
print(f"소수점 문제가 있는 스킬: {len(long_decimal_skills)}")
for item in long_decimal_skills:
print(f" [{item['stalker']}] {item['skillId']} - {item['name']}")
print(f" 원본: {item['value']}")
print(f" 반올림: {item['rounded']}")

View File

@ -0,0 +1,21 @@
"""JSON 파일 구조 확인"""
import json
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"데이터 타입: {type(data)}")
if isinstance(data, dict):
print(f"최상위 키: {list(data.keys())[:10]}")
# CharacterAbility 관련 키 찾기
char_keys = [k for k in data.keys() if 'Character' in k or 'Ability' in k]
print(f"\nCharacter/Ability 관련 키 ({len(char_keys)}개):")
for k in char_keys[:5]:
print(f" - {k}")
elif isinstance(data, list):
print(f"리스트 길이: {len(data)}")
print(f"첫 항목 타입: {type(data[0])}")
if isinstance(data[0], dict):
print(f"첫 항목 키: {list(data[0].keys())}")

View File

@ -0,0 +1,80 @@
"""리옌 연화, 정조준 몽타주 확인"""
import json
with open('../../원본데이터/AnimMontage.json', 'r', encoding='utf-8') as f:
montage_data = json.load(f)
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
dt_data = json.load(f)
# DT_Skill에서 SK190201, SK190101의 몽타주 이름 찾기
dt_skill = None
for asset in dt_data.get('Assets', []):
if asset.get('AssetName') == 'DT_Skill':
dt_skill = asset
break
if not dt_skill:
print("DT_Skill을 찾을 수 없습니다.")
exit(1)
print("=== 리옌 스킬 몽타주 확인 ===\n")
target_skills = ['SK190201', 'SK190101']
skill_montages = {}
for row in dt_skill.get('Rows', []):
row_name = row.get('RowName', '')
if row_name in target_skills:
row_data = row.get('Data', {})
montage_path = row_data.get('montage', '')
skill_name = row_data.get('name', '')
print(f"[{row_name}] {skill_name}")
print(f" Montage Path: {montage_path}")
# 몽타주 이름 추출
if montage_path:
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
skill_montages[row_name] = montage_name
print(f" Montage Name: {montage_name}\n")
# 각 몽타주에서 노티파이 확인
print("\n=== 몽타주 노티파이 확인 ===\n")
for skill_id, montage_name in skill_montages.items():
for asset in montage_data.get('Assets', []):
if asset.get('AssetName') == montage_name:
print(f"[{skill_id}] {montage_name}")
notifies = asset.get('AnimNotifies', [])
print(f" 총 노티파이: {len(notifies)}\n")
for idx, notify in enumerate(notifies):
notify_class = notify.get('NotifyClass', '')
# SimpleSendEvent 노티파이
if 'SimpleSendEvent' in notify_class:
print(f" [{idx}] SimpleSendEvent")
print(f" NotifyClass: {notify_class}")
if 'CustomProperties' in notify:
custom_props = notify['CustomProperties']
event_tag = custom_props.get('Event Tag', '')
print(f" Event Tag: {event_tag}")
# Event.SpawnProjectile 확인
if 'SpawnProjectile' in event_tag:
print(f" >>> Event.SpawnProjectile 발견! (공격 스킬)")
print()
# Projectile 노티파이
if 'Projectile' in notify_class:
print(f" [{idx}] Projectile 노티파이")
print(f" NotifyClass: {notify_class}")
if 'ProjectileShot' in notify_class:
print(f" >>> ProjectileShot 발견! (공격 스킬)")
print()
print("-" * 60)
print()

View File

@ -0,0 +1,89 @@
"""리옌 연화, 정조준 몽타주 확인 (수정)"""
import json
with open('../../원본데이터/AnimMontage.json', 'r', encoding='utf-8') as f:
montage_data = json.load(f)
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
dt_data = json.load(f)
# DT_Skill에서 SK190201, SK190101의 몽타주 이름 찾기
dt_skill = None
for asset in dt_data.get('Assets', []):
if asset.get('AssetName') == 'DT_Skill':
dt_skill = asset
break
print("=== 리옌 스킬 몽타주 확인 ===\n")
target_skills = {
'SK190201': '연화',
'SK190101': '정조준'
}
skill_montages = {}
for row in dt_skill.get('Rows', []):
row_name = row.get('RowName', '')
if row_name in target_skills:
row_data = row.get('Data', {})
use_montages = row_data.get('useMontages', [])
skill_name = row_data.get('name', '')
print(f"[{row_name}] {skill_name}")
print(f" useMontages: {len(use_montages)}")
if use_montages:
montage_path = use_montages[0]
print(f" Path: {montage_path}")
# 몽타주 이름 추출
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
skill_montages[row_name] = montage_name
print(f" Name: {montage_name}\n")
# 각 몽타주에서 노티파이 확인
print("\n=== 몽타주 노티파이 확인 ===\n")
for skill_id, montage_name in skill_montages.items():
for asset in montage_data.get('Assets', []):
if asset.get('AssetName') == montage_name:
print(f"[{skill_id}] {target_skills[skill_id]} - {montage_name}")
notifies = asset.get('AnimNotifies', [])
print(f" 총 노티파이: {len(notifies)}\n")
found_attack_notify = False
for idx, notify in enumerate(notifies):
notify_class = notify.get('NotifyClass', '')
# SimpleSendEvent 노티파이
if 'SimpleSendEvent' in notify_class:
custom_props = notify.get('CustomProperties', {})
event_tag = custom_props.get('Event Tag', '')
# Event.SpawnProjectile 또는 Event.SkillActivate 확인
if 'SpawnProjectile' in event_tag or 'SkillActivate' in event_tag:
print(f" [{idx}] SimpleSendEvent")
print(f" Event Tag: {event_tag}")
print(f" >>> 공격 스킬 판정!")
found_attack_notify = True
print()
# Projectile 노티파이
if 'Projectile' in notify_class:
print(f" [{idx}] Projectile 노티파이")
print(f" NotifyClass: {notify_class}")
trigger_time = notify.get('TriggerTime', 0)
print(f" TriggerTime: {trigger_time}")
if 'ProjectileShot' in notify_class or 'Trigger_Projectile' in notify_class:
print(f" >>> 공격 스킬 판정!")
found_attack_notify = True
print()
if not found_attack_notify:
print(" *** 공격 노티파이를 찾지 못했습니다. ***\n")
print("-" * 60)
print()

View File

@ -0,0 +1,47 @@
"""AnimMontage 이름 패턴 확인"""
import json
with open('../../원본데이터/AnimMontage.json', 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
# 바란 관련 몽타주 찾기
print("=== 바란(Varan) 관련 몽타주 ===")
varan_montages = [a for a in assets if 'varan' in a.get('AssetName', '').lower() or 'baran' in a.get('AssetName', '').lower()]
for m in varan_montages[:10]:
print(f" - {m.get('AssetName')}")
# 클라드(Clad) 관련 몽타주 찾기
print("\n=== 클라드(Clad) 관련 몽타주 ===")
clad_montages = [a for a in assets if 'clad' in a.get('AssetName', '').lower()]
for m in clad_montages[:10]:
print(f" - {m.get('AssetName')}")
# 리옌(Lian) 관련 몽타주 찾기
print("\n=== 리옌(Lian) 관련 몽타주 ===")
lian_montages = [a for a in assets if 'lian' in a.get('AssetName', '').lower()]
for m in lian_montages[:10]:
print(f" - {m.get('AssetName')}")
# SimpleSendEvent 노티파이가 있는 몽타주 찾기
print("\n=== SimpleSendEvent 노티파이가 있는 몽타주 ===")
count = 0
for asset in assets:
notifies = asset.get('AnimNotifies', [])
for notify in notifies:
notify_class = notify.get('NotifyClass', '')
if 'SimpleSendEvent' in notify_class:
print(f" - {asset.get('AssetName')}")
print(f" NotifyClass: {notify_class}")
# CustomProperties 확인
if 'CustomProperties' in notify:
custom_props = notify['CustomProperties']
print(f" CustomProperties: {custom_props}")
count += 1
if count >= 5: # 처음 5개만
break
if count >= 5:
break

View File

@ -0,0 +1,63 @@
"""AN_SimpleSendEvent 노티파이 구조 확인"""
import json
# AnimMontage.json 로드
with open('../../원본데이터/AnimMontage.json', 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
print(f"총 AnimMontage Assets: {len(assets)}\n")
# 문제가 되는 스킬들의 몽타주 찾기
target_skills = {
'SK130301': '바란 일격분쇄', # Event.SkillActivate
'SK150201': '클라드 다시 흙으로', # Event.SkillActivate
'SK190201': '리옌 연화', # Event.SpawnProjectile
'SK190101': '리옌 정조준', # ProjectileShot
}
# 각 스킬 몽타주에서 SimpleSendEvent 찾기
print("=== SimpleSendEvent 노티파이 검색 ===\n")
for asset in assets:
asset_name = asset.get('AssetName', '')
# 해당 스킬 ID가 AssetName에 포함되어 있는지 확인
for skill_id in target_skills.keys():
if skill_id in asset_name:
print(f"[{skill_id}] {target_skills[skill_id]}")
print(f" 몽타주: {asset_name}")
# AnimNotifies 확인
notifies = asset.get('AnimNotifies', [])
print(f" 총 노티파이: {len(notifies)}\n")
for idx, notify in enumerate(notifies):
notify_class = notify.get('NotifyClass', '')
# SimpleSendEvent 노티파이 찾기
if 'SimpleSendEvent' in notify_class:
print(f" [{idx}] SimpleSendEvent 발견!")
print(f" NotifyClass: {notify_class}")
print(f" 노티파이 키: {list(notify.keys())}")
# CustomProperties 확인
if 'CustomProperties' in notify:
custom_props = notify['CustomProperties']
print(f" CustomProperties 타입: {type(custom_props)}")
print(f" CustomProperties 내용:")
if isinstance(custom_props, dict):
for k, v in custom_props.items():
print(f" - {k}: {v}")
else:
print(f" {custom_props}")
print()
# ProjectileShot 노티파이도 확인 (SK190101)
if 'ProjectileShot' in notify_class or 'Projectile' in notify_class:
print(f" [{idx}] Projectile 노티파이 발견!")
print(f" NotifyClass: {notify_class}")
print()
print("-" * 60)
print()

View File

@ -0,0 +1,43 @@
"""SK150201 몽타주 확인"""
import json
from pathlib import Path
# 최신 출력 디렉토리
result_base = Path(__file__).parent.parent.parent / "분석결과"
v2_dirs = sorted([d for d in result_base.iterdir() if d.is_dir() and d.name.endswith('_v2')],
key=lambda d: d.stat().st_mtime)
latest_dir = v2_dirs[-1]
with open(latest_dir / 'intermediate_data.json', 'r', encoding='utf-8') as f:
data = json.load(f)
clad = data['clad']
# SK150201 찾기
skills = [s for s in clad['defaultSkills'] if s and s.get('skillId') == 'SK150201']
if not skills:
print("SK150201을 찾을 수 없습니다.")
exit(1)
skill = skills[0]
print(f"SK150201: {skill.get('name')}")
print(f"useMontages: {skill.get('useMontages')}\n")
montage_data = skill.get('montageData', [])
print(f"montageData: {len(montage_data)}\n")
for idx, md in enumerate(montage_data):
print(f"[{idx}] {md.get('assetName')}")
print(f" hasAttack: {md.get('hasAttack')}")
print(f" attackNotifies: {len(md.get('attackNotifies', []))}")
print(f" allNotifies: {len(md.get('allNotifies', []))}")
# allNotifies에서 SimpleSendEvent 찾기
all_notifies = md.get('allNotifies', [])
for notify in all_notifies:
notify_class = notify.get('NotifyClass', '')
if 'SimpleSendEvent' in notify_class:
custom_props = notify.get('CustomProperties', {})
event_tag = custom_props.get('Event Tag', '')
print(f" SimpleSendEvent found: {event_tag}")
print()

View File

@ -0,0 +1,32 @@
"""DT_Skill 구조 확인"""
import json
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
dt_skill = None
for asset in data.get('Assets', []):
if asset.get('AssetName') == 'DT_Skill':
dt_skill = asset
break
if not dt_skill:
print("DT_Skill을 찾을 수 없습니다.")
exit(1)
print("=== DT_Skill 구조 확인 ===\n")
# SK190201 찾기
rows = dt_skill.get('Rows', [])
for row in rows:
row_name = row.get('RowName', '')
if row_name == 'SK190201':
row_data = row.get('Data', {})
print(f"SK190201 데이터 키:")
for key in sorted(row_data.keys()):
value = row_data[key]
if isinstance(value, str) and len(value) > 100:
print(f" - {key}: (긴 문자열, {len(value)}자)")
else:
print(f" - {key}: {value}")
break

View File

@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""Projectile 노티파이 조사 스크립트"""
import json
from pathlib import Path
from collections import defaultdict
# AnimMontage.json 로드
montage_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/원본데이터/AnimMontage.json")
with open(montage_file, 'r', encoding='utf-8') as f:
montage_data = json.load(f)
# validated_data.json 로드 (유틸리티 스킬 확인용)
val_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/분석결과/20251024_210822_v2/validated_data.json")
with open(val_file, 'r', encoding='utf-8') as f:
val_data = json.load(f)
print("=" * 80)
print("Projectile 노티파이 조사")
print("=" * 80)
# 1. 우르드 다발 화살 몽타주 확인
print("\n=== 예시: Urud 다발 화살 (SK110205) ===")
assets = montage_data.get('Assets', [])
multi_arrow = [a for a in assets if a.get('AssetName') == 'AM_PC_Urud_Base_B_Skill_MultiArrow']
if multi_arrow:
m = multi_arrow[0]
notifies = m.get('AnimNotifies', [])
print(f"Montage: {m.get('AssetName')}")
print(f"Total notifies: {len(notifies)}")
print("\nNotify Classes:")
for n in notifies:
notify_class = n.get('NotifyClass', 'N/A')
notify_state = n.get('NotifyStateClass', 'N/A')
if notify_class != 'N/A':
print(f" - NotifyClass: {notify_class}")
if notify_state != 'N/A':
print(f" - NotifyStateClass: {notify_state}")
else:
print("몽타주를 찾을 수 없음!")
# 2. 모든 PC 스킬 몽타주에서 Projectile 관련 노티파이 패턴 수집
print("\n" + "=" * 80)
print("모든 PC 스킬 몽타주에서 Projectile 패턴 조사")
print("=" * 80)
projectile_patterns = defaultdict(int)
pc_skill_montages = [a for a in assets if 'PC' in a.get('AssetPath', '') and 'Skill' in a.get('AssetPath', '')]
print(f"\n총 PC 스킬 몽타주: {len(pc_skill_montages)}")
for montage in pc_skill_montages:
notifies = montage.get('AnimNotifies', [])
for notify in notifies:
notify_class = notify.get('NotifyClass', '')
notify_state = notify.get('NotifyStateClass', '')
# Projectile 또는 관련 키워드 포함
keywords = ['Projectile', 'projectile', 'Shot', 'shot', 'Fire', 'Spawn', 'Arrow', 'Bullet']
for keyword in keywords:
if keyword in notify_class:
projectile_patterns[notify_class] += 1
if keyword in notify_state:
projectile_patterns[notify_state] += 1
print(f"\nProjectile 관련 노티파이 패턴 발견: {len(projectile_patterns)}")
for pattern, count in sorted(projectile_patterns.items(), key=lambda x: x[1], reverse=True):
print(f" {pattern}: {count}")
# 3. 유틸리티로 판정된 스킬 중 Projectile 노티파이가 있는 스킬 찾기
print("\n" + "=" * 80)
print("유틸리티 판정 스킬 중 Projectile 노티파이 보유 스킬")
print("=" * 80)
utility_with_projectile = []
for stalker_id, stalker_data in val_data.items():
skills = stalker_data.get('skills', {})
for skill_id, skill in skills.items():
is_utility = skill.get('isUtility', False)
if is_utility:
# 몽타주 데이터 확인
montage_data_list = skill.get('montageData', [])
for montage_info in montage_data_list:
all_notifies = montage_info.get('allNotifies', [])
has_projectile = False
projectile_notifies = []
for notify in all_notifies:
notify_class = notify.get('NotifyClass', '')
notify_state = notify.get('NotifyStateClass', '')
for keyword in keywords:
if keyword in notify_class or keyword in notify_state:
has_projectile = True
projectile_notifies.append(notify_class or notify_state)
if has_projectile:
utility_with_projectile.append({
'stalker': stalker_id,
'skillId': skill_id,
'skillName': skill.get('name', 'N/A'),
'montage': montage_info.get('assetName', 'N/A'),
'projectileNotifies': projectile_notifies,
'damageRate': skill.get('skillDamageRate', 0)
})
print(f"\n유틸리티로 잘못 판정된 가능성이 있는 스킬: {len(utility_with_projectile)}\n")
for item in utility_with_projectile:
print(f"[{item['stalker']}] {item['skillId']} - {item['skillName']}")
print(f" Damage Rate: {item['damageRate']}")
print(f" Montage: {item['montage']}")
print(f" Projectile Notifies: {', '.join(set(item['projectileNotifies']))}")
print()
# 4. 권장 ATTACK_NOTIFY_CLASSES 업데이트
print("=" * 80)
print("권장 ATTACK_NOTIFY_CLASSES 추가 키워드")
print("=" * 80)
# 빈도가 높은 패턴 추출 (5회 이상)
high_frequency = [p for p, c in projectile_patterns.items() if c >= 3]
print("\n추가 권장 키워드 (빈도 3회 이상):")
for pattern in high_frequency[:10]:
print(f" - '{pattern.split('_')[-1] if '_' in pattern else pattern}'")

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""개선사항 검증 스크립트"""
import json
from pathlib import Path
val_file = Path("D:/Work/WorldStalker/DS-전투분석_저장소/분석결과/20251027_081738_v2/validated_data.json")
with open(val_file, 'r', encoding='utf-8') as f:
data = json.load(f)
print("=" * 80)
print("개선사항 검증")
print("=" * 80)
# 1. descValues 소수점 반올림
print("\n1. DescValues 소수점 반올림")
print("-" * 80)
cazi = data['cazimord']
sk170101 = cazi['skills']['SK170101']
print(f"SK170101 (흘리기):")
print(f" DescValues: {sk170101.get('descValues')}")
print(f" DescFormatted: {sk170101.get('descFormatted')[:100]}...")
print(f" ✅ 소수점 반올림 확인: 3.8, 6.8")
# 2. effectiveAttackTime 추출
print("\n2. EffectiveAttackTime (Projectile 발사 시점)")
print("-" * 80)
urud = data['urud']
sk110205 = urud['skills']['SK110205']
montages = sk110205.get('montageData', [])
if montages:
m = montages[0]
print(f"SK110205 (다발 화살):")
print(f" Montage: {m.get('assetName')}")
print(f" ActualDuration: {m.get('actualDuration'):.2f}")
print(f" EffectiveAttackTime: {m.get('effectiveAttackTime'):.2f}")
print(f" ProjectileTriggerTimes: {m.get('projectileTriggerTimes')}")
time_saved = m.get('actualDuration', 0) - m.get('effectiveAttackTime', 0)
print(f"{time_saved:.2f}초 빠르게 공격 가능")
# 3. CastingTime 수집
print("\n3. CastingTime 수집")
print("-" * 80)
nave = data['nave']
sk120202 = nave['skills']['SK120202']
print(f"SK120202 (화염벽):")
print(f" CastingTime: {sk120202.get('castingTime')}")
print(f" ✅ 시전시간 수집 확인")
# 4. DoT 스킬 마킹
print("\n4. DoT 스킬 마킹")
print("-" * 80)
sk110204 = urud['skills']['SK110204']
print(f"SK110204 (독성 화살):")
print(f" IsDot: {sk110204.get('isDot')}")
print(f" DamageRate: {sk110204.get('skillDamageRate')}")
print(f" ✅ DoT 스킬 마킹 확인")
print("\n" + "=" * 80)
print("모든 개선사항 검증 완료!")
print("=" * 80)

View File

@ -0,0 +1,143 @@
"""v2.3 개선사항 검증 스크립트"""
import json
from pathlib import Path
# 최신 출력 디렉토리 찾기
result_base = Path(__file__).parent.parent.parent / "분석결과"
v2_dirs = sorted([d for d in result_base.iterdir() if d.is_dir() and d.name.endswith('_v2')],
key=lambda d: d.stat().st_mtime)
latest_dir = v2_dirs[-1]
print(f"검증 디렉토리: {latest_dir.name}\n")
# validated_data.json 로드
with open(latest_dir / 'validated_data.json', 'r', encoding='utf-8') as f:
data = json.load(f)
print("=" * 70)
print("v2.3 개선사항 검증")
print("=" * 70)
# 1. 우르드/리옌 평타 effectiveAttackTime 검증
print("\n[1] 우르드/리옌 평타 effectiveAttackTime (Projectile TriggerTime)")
print("-" * 70)
for stalker_id in ['urud', 'lian']:
stalker = data.get(stalker_id, {})
basic_attacks = stalker.get('basicAttacks', {})
for weapon_type, attacks in basic_attacks.items():
for attack in attacks:
montage_name = attack['montageName']
actual_duration = attack['actualDuration']
effective_time = attack.get('effectiveAttackTime', actual_duration)
projectile_triggers = attack.get('projectileTriggerTimes', [])
if projectile_triggers:
saved_time = actual_duration - effective_time
print(f"{stalker_id}/{weapon_type} 평타:")
print(f" Montage: {montage_name}")
print(f" ActualDuration: {actual_duration:.2f}")
print(f" EffectiveAttackTime: {effective_time:.2f}")
print(f" ProjectileTriggers: {projectile_triggers}")
print(f" => {saved_time:.2f}초 빠름!")
# 2. 공격 스킬 판정 검증
print("\n[2] 공격 스킬 판정 (SimpleSendEvent Event Tag)")
print("-" * 70)
test_skills = {
'SK130301': '바란 일격분쇄 (Event.SkillActivate)',
'SK150201': '클라드 다시 흙으로 (Event.SkillActivate)',
'SK190201': '리옌 연화 (Event.SpawnProjectile)',
'SK190101': '리옌 정조준 (ProjectileShot)',
}
for skill_id, expected_desc in test_skills.items():
found = False
for stalker_id, stalker in data.items():
all_skills = (stalker.get('defaultSkills', []) +
[stalker.get('subSkill')] +
[stalker.get('ultimateSkill')])
for skill in all_skills:
if skill and skill.get('skillId') == skill_id:
is_attack = len(skill.get('montageData', [])) > 0 and skill['montageData'][0].get('hasAttack', False)
status = "공격 스킬" if is_attack else "유틸리티"
print(f"{skill_id}: {skill.get('name')} => {status}")
print(f" Expected: {expected_desc}")
# 몽타주 데이터 확인
if skill.get('montageData'):
montage = skill['montageData'][0]
attack_notifies = montage.get('attackNotifies', [])
print(f" AttackNotifies: {len(attack_notifies)}")
# SimpleSendEvent 확인
for notify in attack_notifies:
if 'SimpleSendEvent' in notify.get('notifyClass', ''):
event_tag = notify.get('customProperties', {}).get('Event Tag', '')
print(f" - SimpleSendEvent: {event_tag}")
found = True
break
if found:
break
if not found:
print(f"{skill_id}: NOT FOUND")
# 3. 유틸리티 스킬 확인 (공격 노티파이 없음)
print("\n[3] 유틸리티 스킬 (공격 노티파이 없음)")
print("-" * 70)
utility_skills = {
'SK110207': '우르드 Reload',
'SK190209': '리옌 재장전'
}
for skill_id, expected_name in utility_skills.items():
found = False
for stalker_id, stalker in data.items():
all_skills = (stalker.get('defaultSkills', []) +
[stalker.get('subSkill')] +
[stalker.get('ultimateSkill')])
for skill in all_skills:
if skill and skill.get('skillId') == skill_id:
has_attack = len(skill.get('montageData', [])) > 0 and skill['montageData'][0].get('hasAttack', False)
status = "공격" if has_attack else "유틸리티"
print(f"{skill_id}: {skill.get('name')} => {status}")
if skill.get('montageData'):
montage = skill['montageData'][0]
attack_notifies_count = len(montage.get('attackNotifies', []))
print(f" AttackNotifies: {attack_notifies_count}")
found = True
break
if found:
break
# 4. 레네 소환체 섹션 확인
print("\n[4] 레네 소환체 섹션")
print("-" * 70)
rene = data.get('rene', {})
summons = rene.get('summons', {})
if summons:
print(f"레네 소환체: {len(summons)}")
for summon_name, summon_data in summons.items():
print(f"\n {summon_name}:")
print(f" SummonSkillId: {summon_data.get('summonSkillId')}")
print(f" SummonSkillName: {summon_data.get('summonSkillName')}")
print(f" SkillDamageRate: {summon_data.get('skillDamageRate')}")
print(f" AttackInterval: {summon_data.get('attackInterval')}")
print(f" DotType: {summon_data.get('dotType', 'None')}")
else:
print("레네 소환체 데이터 없음!")
print("\n" + "=" * 70)
print("검증 완료!")
print("=" * 70)

214
분석도구/v2/config.py Normal file
View File

@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
스토커 데이터 분석 v2 - 설정 파일
"""
from pathlib import Path
from datetime import datetime
# 프로젝트 루트
PROJECT_ROOT = Path(__file__).parent.parent.parent
# 원본 데이터 경로
DATA_DIR = PROJECT_ROOT / "원본데이터"
DATATABLE_JSON = DATA_DIR / "DataTable.json"
BLUEPRINT_JSON = DATA_DIR / "Blueprint.json"
ANIMMONTAGE_JSON = DATA_DIR / "AnimMontage.json"
CURVETABLE_JSON = DATA_DIR / "CurveTable.json"
# 출력 디렉토리 (타임스탬프 자동 생성)
def get_output_dir(create_new: bool = False) -> Path:
"""
출력 디렉토리 가져오기
- create_new=True: 새 타임스탬프 디렉토리 생성
- create_new=False: 가장 최근 디렉토리 사용 (없으면 생성)
"""
result_base = PROJECT_ROOT / "분석결과"
result_base.mkdir(parents=True, exist_ok=True)
if create_new:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return result_base / f"{timestamp}_v2"
# 기존 v2 디렉토리 중 가장 최근 것 찾기 (수정 시간 기준)
v2_dirs = [d for d in result_base.iterdir() if d.is_dir() and d.name.endswith('_v2')]
if v2_dirs:
# 수정 시간 기준으로 정렬
v2_dirs_sorted = sorted(v2_dirs, key=lambda d: d.stat().st_mtime)
return v2_dirs_sorted[-1] # 가장 최근 디렉토리
# 없으면 새로 생성
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return result_base / f"{timestamp}_v2"
OUTPUT_DIR = get_output_dir()
# 스토커 목록 (순서: 기존 문서 기준)
STALKERS = [
'hilda', # 1. 힐다 - 방어형 전사
'urud', # 2. 우르드 - 원거리 딜러
'nave', # 3. 네이브 - 마법사
'baran', # 4. 바란 - 파워 전사
'rio', # 5. 리오 - 암살자
'clad', # 6. 클라드 - 성직자
'rene', # 7. 레네 - 소환사
'sinobu', # 8. 시노부 - 닌자
'lian', # 9. 리안 - 레인저
'cazimord' # 10. 카지모르드 - 평타 중심 전사
]
# 스토커 정보 (영문 이름, 한글 이름, 직업)
STALKER_INFO = {
'hilda': {'english': 'Hilda', 'name': '힐다', 'job': '전사', 'role': '탱커'},
'urud': {'english': 'Urud', 'name': '우르드', 'job': '원거리', 'role': '원거리 딜러'},
'nave': {'english': 'Nave', 'name': '네이브', 'job': '마법사', 'role': '광역 마법 딜러'},
'baran': {'english': 'Baran', 'name': '바란', 'job': '전사', 'role': '고화력 전사'},
'rio': {'english': 'Rio', 'name': '리오', 'job': '암살자', 'role': '빠른 근접 암살자'},
'clad': {'english': 'Clad', 'name': '클라드', 'job': '성직자', 'role': '서포터/힐러'},
'rene': {'english': 'Rene', 'name': '레네', 'job': '소환사', 'role': '소환사/마법 딜러'},
'sinobu': {'english': 'Sinobu', 'name': '시노부', 'job': '닌자', 'role': '기동형 암살자'},
'lian': {'english': 'Lian', 'name': '리안', 'job': '레인저', 'role': '정밀 원거리 딜러'},
'cazimord': {'english': 'Cazimord', 'name': '카지모르드', 'job': '전사', 'role': '고숙련도 하이브리드 전사'}
}
# 분석 기준 (기존 문서 기준)
ANALYSIS_BASELINE = {
'level': 20,
'gear_score': 400,
'play_style': '최적 플레이',
'rune_effect': {
'cooltime_reduction': 0.25, # 왜곡 룬 -25% 쿨타임
}
}
# DoT 스킬 목록
DOT_SKILLS = {
'SK110204': {'stalker': 'urud', 'name': '독성 화살', 'dot_type': 'Poison'},
'SK160203': {'stalker': 'rene', 'name': '독기 화살', 'dot_type': 'Bleed'},
'SK170201': {'stalker': 'cazimord', 'name': '작열', 'dot_type': 'Burn'}, # 수정: SK170203 -> SK170201
'SK160202': {'stalker': 'rene', 'name': '정령 소환: 화염', 'dot_type': 'Burn'} # Ifrit 화상
}
# DoT 피해 상세 정보
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 계산 필요)
SUMMON_SKILLS = {
'SK160202': {
'stalker': 'rene',
'name': '정령 소환: 화염',
'summon': 'Ifrit',
'type': 'npc' # DT_NPCAbility 사용
},
'SK160206': {
'stalker': 'rene',
'name': '정령 소환: 냉기',
'summon': 'Shiva',
'type': 'special', # DT_NPCAbility 사용 안 함
'montage': 'AM_Sum_Elemental_Ice_Attack_N01', # 직접 지정
'attack_interval_bonus': 1.0 # 공격 주기에 추가되는 시간(초)
}
}
# 유틸리티 스킬 (DPS 제외 - 확실한 것만 명시)
# 공격 노티파이가 없는 스킬들
UTILITY_SKILLS = {
'SK100204': 'hilda - 도발',
'SK110201': 'urud - 덫 설치',
'SK110207': 'urud - Reload', # 재장전
'SK120101': 'nave - 마력 충전',
'SK130101': 'baran - 무기 막기',
'SK150206': 'clad - 치유',
'SK150202': 'clad - 신성한 빛 (DOT 제거)',
'SK180205': 'sinobu - 바꿔치기 (피격 시 효과)',
'SK180206': 'sinobu - 인술 칠흑안개',
'SK190209': 'lian - 재장전', # 재장전
'SK100101': 'hilda - 방패 들기',
'SK150101': 'clad - 방패 방어',
'SK170101': 'cazimord - Parrying',
}
# 공격 스킬로 확정된 스킬 (노티파이 확인 완료)
# 주의: 아래 스킬들은 UTILITY_SKILLS에서 제외됨
CONFIRMED_ATTACK_SKILLS = {
'SK130301': 'baran - 일격분쇄 (Event.SkillActivate)',
'SK150201': 'clad - 다시 흙으로 (Event.SkillActivate)',
'SK190201': 'lian - 연화 (Event.SpawnProjectile)',
'SK190101': 'lian - 정조준 (Projectile Shot)', # UTILITY에서 제거됨
}
# 공격 스킬 판별 기준 (우선순위)
#
# 우선순위 1: AnimNotify의 NotifyName에 다음 키워드 포함 (부분 매칭)
# - 실질적으로 데미지가 발생하는 시점을 나타내는 노티파이
ATTACK_NOTIFY_KEYWORDS = [
'AttackWithEquip', # 무기 공격 (근접)
'Projectile', # 투사체 발사 (AN_Projectile_C, AN_Trigger_Projectile_Shot_C 등)
'SkillActive', # 스킬 활성화 (AN_Trigger_Skill_Active_C)
]
# 우선순위 2: AN_SimpleSendEvent 노티파이의 Event Tag
# - 1순위에 해당되지 않을 때 2순위로 확인
ATTACK_EVENT_TAGS = [
'Event.SkillActivate', # 스킬 활성화 (바란, 클라드 등)
'Event.SpawnProjectile', # 투사체 생성 (리옌 연화 등)
]
# BaseDamage 계산식 (기존 분석 기준)
BASE_DAMAGE_FORMULA = {
'physical_str': lambda stats: (stats['str'] + 80) * 1.20,
'physical_dex': lambda stats: (stats['dex'] + 80) * 1.20,
'magical': lambda stats: (stats['int'] + 80) * 1.10,
'support': lambda stats: (stats.get('wis', stats.get('con', 0)) + 80) * 1.00
}
# 검증 기준
VALIDATION_RULES = {
'stat_total': 75, # 모든 스토커 스탯 합계
'hp': 100,
'mp': 50,
'mana_regen': 0.2,
'skill_damage_rate_min': 0.0,
'cooltime_min': 0.0
}
# 시퀀스 길이 계산 규칙
SEQUENCE_CALCULATION_RULES = {
# 합산에서 제외할 몽타주 키워드 (대소문자 구분 없음)
'exclude_keywords': ['Ready', 'Equipment'],
# 평균값으로 계산할 스킬 (몽타주를 번갈아 사용)
'average_skills': ['SK160101'], # 레네 - 할퀴기
# 특정 몽타주를 제외할 스킬 (스킬ID: [제외할 몽타주 이름들])
'exclude_montages': {
'SK170201': ['AM_PC_Cazimord_B_Skill_Flash'], # 카지모르드 - 섬광 (첫 번째 몽타주 제외)
},
# 인덱스로 제외할 몽타주 (스킬ID: [제외할 인덱스들, 0-based])
'exclude_montage_indices': {
'SK190205': [1], # 리옌 - 비연사 (두 번째 중복 몽타주 제외)
},
# 몽타주 태그 표시
'montage_tags': {
'Ready': '[준비]',
'Equipment': '[장비]'
}
}

View File

@ -0,0 +1,672 @@
#!/usr/bin/env python3
"""
스토커 데이터 통합 추출 스크립트 v2
모든 JSON 소스에서 데이터를 추출하여 중간 데이터 파일 생성
- DT_Skill: 스킬 상세 정보
- DT_CharacterStat/Ability: 스토커 기본 정보
- Blueprint: 스킬 변수 (ActivationOrderGroup 등)
- AnimMontage: 평타/스킬 타이밍, 공격 노티파이
"""
import json
import sys
import re
from pathlib import Path
from typing import Dict, List, Any, Optional
# config 임포트
sys.path.append(str(Path(__file__).parent))
import config
def format_description(desc: str, desc_values: List) -> str:
"""
desc 문자열의 {0}, {1}, {2} 등을 descValues 배열 값으로 치환
Args:
desc: 원본 설명 문자열 (예: "방패를 들어 {0}초 동안 반격 자세를 취합니다. 반격 성공 시 {1}%만큼 물리 피해를 줍니다.")
desc_values: 값 배열 (예: [5, 80])
Returns:
치환된 설명 문자열 (예: "방패를 들어 5초 동안 반격 자세를 취합니다. 반격 성공 시 80%만큼 물리 피해를 줍니다.")
"""
if not desc or not desc_values:
return desc
# {0}, {1}, {2} 등을 descValues로 치환
result = desc
for i, value in enumerate(desc_values):
placeholder = f"{{{i}}}"
result = result.replace(placeholder, str(value))
# 줄바꿈 제거 (마크다운 호환성)
result = result.replace('\n', ' ').replace('\r', ' ')
return result
def load_json(file_path: Path) -> Dict:
"""JSON 파일 로드"""
print(f"Loading: {file_path.name}")
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data
def find_table(datatables: List[Dict], table_name: str) -> Optional[Dict]:
"""DataTable.json에서 특정 테이블 찾기"""
for dt in datatables:
if dt.get('AssetName') == table_name:
return dt
return None
def find_asset_by_name(assets: List[Dict], name_pattern: str) -> List[Dict]:
"""에셋 이름 패턴으로 검색"""
return [a for a in assets if name_pattern in a.get('AssetName', '')]
def extract_character_stats(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_CharacterStat에서 스토커 기본 정보 추출
Returns:
{stalker_id: {name, job, stats, skills, ...}}
"""
print("\n=== DT_CharacterStat 추출 ===")
char_stat_table = find_table(datatables, 'DT_CharacterStat')
if not char_stat_table:
print("[WARN] DT_CharacterStat 테이블을 찾을 수 없습니다.")
return {}
stalker_data = {}
for row in char_stat_table.get('Rows', []):
row_name = row['RowName']
if row_name not in config.STALKERS:
continue
data = row['Data']
# 스토커 이름 포맷: "English (Korean)"
korean_name = data.get('name', '')
info = config.STALKER_INFO.get(row_name, {})
english_name = info.get('english', row_name.capitalize())
formatted_name = f"{english_name} ({korean_name})" if korean_name else english_name
stalker_data[row_name] = {
'id': row_name,
'name': formatted_name, # 영문(한글) 형식
'koreanName': korean_name, # 순수 한글 이름
'englishName': english_name, # 순수 영문 이름
'jobName': data.get('jobName', ''),
'stats': {
'str': data.get('str', 0),
'dex': data.get('dex', 0),
'int': data.get('int', 0),
'con': data.get('con', 0),
'wis': data.get('wis', 0)
},
'hp': data.get('hP', 0),
'mp': data.get('mP', 0),
'manaRegen': round(data.get('manaRegen', 0), 2), # 소수점 2자리
'physicalDamage': data.get('physicalDamage', 0),
'magicalDamage': data.get('magicalDamage', 0),
'criticalPer': data.get('criticalPer', 5), # 크리티컬 확률
'criticalDamage': data.get('criticalDamage', 0), # 크리티컬 추가 피해
'defaultSkills': data.get('defaultSkills', []),
'subSkill': data.get('subSkill', ''),
'ultimateSkill': data.get('ultimateSkill', ''),
'equipableTypes': data.get('equipableTypes', []),
'ultimatePoint': data.get('ultimatePoint', 0),
'source': 'DT_CharacterStat'
}
print(f" [OK] {stalker_data[row_name]['name']} ({row_name})")
return stalker_data
def extract_character_abilities(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_CharacterAbility에서 평타 몽타주 정보 추출
Returns:
{stalker_id: {attackMontageMap, abilities}}
"""
print("\n=== DT_CharacterAbility 추출 ===")
char_ability_table = find_table(datatables, 'DT_CharacterAbility')
if not char_ability_table:
print("⚠️ DT_CharacterAbility 테이블을 찾을 수 없습니다.")
return {}
stalker_abilities = {}
for row in char_ability_table.get('Rows', []):
row_name = row['RowName']
if row_name not in config.STALKERS:
continue
data = row['Data']
stalker_abilities[row_name] = {
'attackMontageMap': data.get('attackMontageMap', {}),
'abilities': data.get('abilities', []),
'source': 'DT_CharacterAbility'
}
# 평타 콤보 수 계산
combo_counts = {}
for weapon_type, montage_data in stalker_abilities[row_name]['attackMontageMap'].items():
montage_array = montage_data.get('montageArray', [])
combo_counts[weapon_type] = len(montage_array)
print(f" [OK] {row_name}: {combo_counts}")
return stalker_abilities
def extract_skills(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_Skill에서 모든 스킬 정보 추출
Returns:
{skill_id: {skill data}}
"""
print("\n=== DT_Skill 추출 ===")
skill_table = find_table(datatables, 'DT_Skill')
if not skill_table:
print("⚠️ DT_Skill 테이블을 찾을 수 없습니다.")
return {}
all_skills = {}
for row in skill_table.get('Rows', []):
skill_id = row['RowName']
data = row['Data']
# 스토커 스킬만 추출
stalker_name = data.get('stalkerName', '')
if stalker_name not in config.STALKERS:
continue
# 설명 처리: desc에서 {0}, {1} 등을 descValues로 치환
desc_raw = data.get('desc', '')
desc_values_raw = data.get('descValues', [])
# descValues의 float 값을 소수점 둘째자리로 반올림
desc_values = []
for val in desc_values_raw:
if isinstance(val, float):
desc_values.append(round(val, 2))
else:
desc_values.append(val)
desc_formatted = format_description(desc_raw, desc_values)
all_skills[skill_id] = {
'skillId': skill_id,
'stalkerName': stalker_name,
'name': data.get('name', ''),
'desc': desc_raw, # 원본 desc (변수 포함)
'descFormatted': desc_formatted, # 변수 치환된 desc
'descValues': desc_values, # descValues 배열
'simpleDesc': data.get('simpleDesc', ''),
'bIsUltimate': data.get('bIsUltimate', False),
'bIsStackable': data.get('bIsStackable', False),
'maxStackCount': data.get('maxStackCount', 0),
'skillDamageRate': data.get('skillDamageRate', 0),
'skillAttackType': data.get('skillAttackType', ''),
'skillElementType': data.get('skillElementType', ''),
'manaCost': data.get('manaCost', 0),
'coolTime': data.get('coolTime', 0),
'castingTime': data.get('castingTime', 0),
'activeDuration': data.get('activeDuration', 0), # 소환수 지속시간
'activeRange': data.get('activeRange', {}), # tick, count, dist 등
'useMontages': data.get('useMontages', []),
'gameplayEffectSet': data.get('gameplayEffectSet', []),
'abilityClass': data.get('abilityClass', ''),
'icon': data.get('icon', ''),
'bUsable': data.get('bUsable', False),
'bUnSelectable': data.get('bUnSelectable', False),
'source': 'DT_Skill'
}
print(f" [OK] 총 {len(all_skills)}개 스킬 추출")
# 스토커별 카운트
stalker_counts = {}
for skill in all_skills.values():
stalker = skill['stalkerName']
stalker_counts[stalker] = stalker_counts.get(stalker, 0) + 1
for stalker_id in config.STALKERS:
count = stalker_counts.get(stalker_id, 0)
print(f" - {stalker_id}: {count}")
return all_skills
def extract_skill_blueprints(blueprints: List[Dict]) -> Dict[str, Dict]:
"""
Blueprint.json에서 GA_Skill_ 블루프린트의 변수 추출
Returns:
{blueprint_name: {variables}}
"""
print("\n=== GA_Skill Blueprint 추출 ===")
skill_blueprints = {}
ga_skills = [bp for bp in blueprints if 'GA_Skill' in bp.get('AssetName', '')]
for bp in ga_skills:
asset_name = bp['AssetName']
variables = {}
for var in bp.get('Variables', []):
var_name = var.get('Name', '')
variables[var_name] = {
'name': var_name,
'type': var.get('Type', var.get('Category', 'unknown')),
'defaultValue': var.get('DefaultValue', 'N/A'),
'source': var.get('Source', 'Blueprint'),
'category': var.get('CategoryName', ''),
'isEditable': var.get('IsEditable', False),
'isBlueprintVisible': var.get('IsBlueprintVisible', False)
}
skill_blueprints[asset_name] = {
'assetName': asset_name,
'assetPath': bp.get('AssetPath', ''),
'parentClass': bp.get('ParentClass', ''),
'variables': variables,
'source': 'Blueprint'
}
print(f" [OK] 총 {len(skill_blueprints)}개 GA_Skill Blueprint 추출")
return skill_blueprints
def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
"""
AnimMontage.json에서 몽타주 타이밍 및 노티파이 추출
- AddNormalAttackPer 추출 (ANS_AttackState_C 노티파이)
Returns:
{montage_name: {timing, notifies, attackMultiplier}}
"""
print("\n=== AnimMontage 추출 ===")
all_montages = {}
pc_montages = [m for m in montages if 'AM_PC_' in m.get('AssetName', '') or 'AM_Sum_' in m.get('AssetName', '')]
for montage in pc_montages:
asset_name = montage['AssetName']
# 공격 노티파이 추출
attack_notifies = []
attack_multiplier = 0.0 # AddNormalAttackPer (기본값 0)
for notify in montage.get('AnimNotifies', []):
notify_class = notify.get('NotifyClass', '')
notify_state_class = notify.get('NotifyStateClass', '')
notify_name = notify.get('NotifyName', '')
custom_props = notify.get('CustomProperties', {})
# ANS_AttackState_C에서 AddNormalAttackPer 추출
if 'ANS_AttackState' in notify_state_class:
add_normal_attack_str = custom_props.get('AddNormalAttackPer', '0')
try:
attack_multiplier = float(add_normal_attack_str)
except (ValueError, TypeError):
attack_multiplier = 0.0
# 공격 판정 로직 (우선순위)
is_attack_notify = False
# 1. NotifyName에 키워드 포함 (부분 매칭)
if any(keyword in notify_name for keyword in config.ATTACK_NOTIFY_KEYWORDS):
is_attack_notify = True
# 2. CustomProperties의 NotifyName 확인 - 1순위 실패 시
if not is_attack_notify:
custom_notify_name = custom_props.get('NotifyName', '')
if custom_notify_name and any(keyword in custom_notify_name for keyword in config.ATTACK_NOTIFY_KEYWORDS):
is_attack_notify = True
# 3. NotifyClass에 키워드 포함 (부분 매칭) - 1, 2순위 실패 시
if not is_attack_notify:
if any(keyword in notify_class for keyword in config.ATTACK_NOTIFY_KEYWORDS):
is_attack_notify = True
# 4. SimpleSendEvent의 Event Tag 확인 (1, 2, 3순위 실패 시)
if not is_attack_notify and 'SimpleSendEvent' in notify_class:
event_tag = custom_props.get('Event Tag', '')
if any(attack_tag in event_tag for attack_tag in config.ATTACK_EVENT_TAGS):
is_attack_notify = True
if is_attack_notify:
attack_notifies.append({
'notifyName': notify_name,
'notifyClass': notify_class,
'notifyStateClass': notify_state_class,
'triggerTime': notify.get('TriggerTime', 0),
'duration': notify.get('Duration', 0),
'notifyType': notify.get('NotifyType', ''),
'customProperties': custom_props
})
# 시퀀스 길이 = SequenceLength / RateScale (actualDuration)
seq_len = montage.get('SequenceLength', 0)
rate_scale = montage.get('RateScale', 1.0)
actual_duration = seq_len / rate_scale if rate_scale > 0 else seq_len
all_montages[asset_name] = {
'assetName': asset_name,
'assetPath': montage.get('AssetPath', ''),
'sequenceLength': seq_len,
'rateScale': rate_scale,
'actualDuration': actual_duration, # 시퀀스 길이 (SequenceLength / RateScale)
'attackMultiplier': attack_multiplier, # AddNormalAttackPer
'sections': montage.get('Sections', []),
'numSections': montage.get('NumSections', 0),
'allNotifies': montage.get('AnimNotifies', []),
'attackNotifies': attack_notifies,
'hasAttack': len(attack_notifies) > 0,
'blendInTime': montage.get('BlendInTime', 0),
'blendOutTime': montage.get('BlendOutTime', 0),
'source': 'AnimMontage'
}
print(f" [OK] 총 {len(all_montages)}개 몽타주 추출 (PC + Summon)")
# 소환수 몽타주 확인
summon_montages = [m for m in all_montages.keys() if 'Summon' in m or 'Sum_' in m]
if summon_montages:
print(f" [INFO] 소환수 관련 몽타주: {len(summon_montages)}")
for sm in summon_montages:
seq_len = all_montages[sm]['sequenceLength']
actual_dur = all_montages[sm]['actualDuration']
has_attack = all_montages[sm]['hasAttack']
print(f" - {sm}: {seq_len:.2f}초 (실제: {actual_dur:.2f}초), 공격={has_attack}")
return all_montages
def extract_npc_abilities(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_NPCAbility에서 소환수(Ifrit, Shiva) 정보 추출
Returns:
{npc_name: {attackMontageMap}}
"""
print("\n=== DT_NPCAbility 추출 ===")
npc_ability_table = find_table(datatables, 'DT_NPCAbility')
if not npc_ability_table:
print("[WARN] DT_NPCAbility 테이블을 찾을 수 없습니다.")
return {}
npc_abilities = {}
summon_names = ['ifrit', 'shiva'] # Rene의 소환수
for row in npc_ability_table.get('Rows', []):
row_name = row['RowName'].lower()
if row_name not in summon_names:
continue
data = row['Data']
attack_map = data.get('attackMontageMap', {})
npc_abilities[row_name] = {
'npcName': row['RowName'],
'attackMontageMap': attack_map,
'source': 'DT_NPCAbility'
}
# 몽타주 개수 출력
for weapon_type, montage_data in attack_map.items():
montage_array = montage_data.get('montageArray', [])
print(f" [OK] {row['RowName']} ({weapon_type}): {len(montage_array)}개 몽타주")
for i, montage_path in enumerate(montage_array):
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
print(f" {i+1}. {montage_name}")
return npc_abilities
def organize_stalker_data(
stalker_stats: Dict,
stalker_abilities: Dict,
all_skills: Dict,
skill_blueprints: Dict,
anim_montages: Dict,
npc_abilities: Dict
) -> Dict[str, Dict]:
"""
스토커별로 모든 데이터를 통합 정리
Returns:
{stalker_id: {모든 데이터}}
"""
print("\n=== 스토커별 데이터 통합 ===")
organized = {}
for stalker_id in config.STALKERS:
if stalker_id not in stalker_stats:
print(f" [WARN] {stalker_id}: 기본 스탯 없음")
continue
stats = stalker_stats[stalker_id]
abilities = stalker_abilities.get(stalker_id, {})
# 스토커의 스킬 목록
skill_ids = stats['defaultSkills'] + [stats['subSkill'], stats['ultimateSkill']]
skill_ids = [sid for sid in skill_ids if sid] # 빈 문자열 제거
# 스킬 상세 정보
skills = {}
for skill_id in skill_ids:
if skill_id not in all_skills:
print(f" [WARN] {stalker_id}: 스킬 {skill_id} 정보 없음")
continue
skill_data = all_skills[skill_id].copy()
# Blueprint 정보 매칭
ability_class = skill_data.get('abilityClass', '')
if ability_class:
# '/Game/Blueprints/Abilities/GA_Skill_XXX.GA_Skill_XXX_C' -> 'GA_Skill_XXX'
bp_name = ability_class.split('/')[-1].split('.')[0]
if bp_name in skill_blueprints:
skill_data['blueprintVariables'] = skill_blueprints[bp_name]['variables']
else:
skill_data['blueprintVariables'] = {}
# AnimMontage 정보 매칭
use_montages = skill_data.get('useMontages', [])
skill_data['montageData'] = []
for montage_path in use_montages:
# '/Script/Engine.AnimMontage'/Game/_Art/.../ AM_XXX.AM_XXX' -> 'AM_XXX'
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
if montage_name in anim_montages:
skill_data['montageData'].append(anim_montages[montage_name])
else:
print(f" [WARN] {skill_id}: 몽타주 {montage_name} 정보 없음")
# DoT 스킬 체크
skill_data['isDot'] = skill_id in config.DOT_SKILLS
# 소환수 스킬 체크 (유틸리티 판별보다 먼저 설정)
skill_data['isSummon'] = skill_id in config.SUMMON_SKILLS
if skill_data['isSummon']:
summon_info = config.SUMMON_SKILLS.get(skill_id, {})
summon_type = summon_info.get('type', 'npc')
if summon_type == 'special':
# Shiva 특수 처리: 직접 몽타주 지정
skill_data['summonMontageData'] = []
montage_name = summon_info.get('montage')
attack_interval_bonus = summon_info.get('attack_interval_bonus', 0)
if montage_name and montage_name in anim_montages:
montage_info = anim_montages[montage_name].copy()
# 공격 주기 = 실제 시간 + 보너스
original_duration = montage_info['actualDuration']
montage_info['attackInterval'] = original_duration + attack_interval_bonus
skill_data['summonMontageData'].append(montage_info)
skill_data['summonType'] = 'special'
else:
# Ifrit 등: DT_NPCAbility에서 추출
summon_name = summon_info.get('summon', '').lower()
if summon_name in npc_abilities:
npc_data = npc_abilities[summon_name]
attack_map = npc_data.get('attackMontageMap', {})
skill_data['summonAttackMap'] = attack_map
# 소환수 몽타주 데이터 추가
skill_data['summonMontageData'] = []
for weapon_type, montage_data in attack_map.items():
montage_array = montage_data.get('montageArray', [])
for montage_path in montage_array:
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
if montage_name in anim_montages:
skill_data['summonMontageData'].append(anim_montages[montage_name])
skill_data['summonType'] = 'npc'
# 유틸리티 스킬 판별 (isSummon 설정 이후에 실행)
skill_data['isUtility'] = is_utility_skill(skill_data)
skills[skill_id] = skill_data
# 평타 몽타주 상세 정보 매칭
attack_montage_map = abilities.get('attackMontageMap', {})
basic_attacks = {}
for weapon_type, montage_data in attack_montage_map.items():
montage_array = montage_data.get('montageArray', [])
basic_attacks[weapon_type] = []
for idx, montage_path in enumerate(montage_array):
montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
if montage_name in anim_montages:
montage_info = anim_montages[montage_name]
basic_attacks[weapon_type].append({
'index': idx + 1,
'montageName': montage_name,
'sequenceLength': montage_info['sequenceLength'],
'rateScale': montage_info['rateScale'],
'actualDuration': montage_info['actualDuration'],
'attackMultiplier': montage_info['attackMultiplier'],
'hasAttack': montage_info['hasAttack']
})
# 소환체 데이터 생성 (레네만)
summons = {}
for skill_id, skill_data in skills.items():
if skill_data.get('isSummon'):
summon_config = config.SUMMON_SKILLS.get(skill_id, {})
summon_name = summon_config.get('summon', 'Unknown')
# 공격 몽타주 정보 추출
attack_montages = []
for montage_data in skill_data.get('summonMontageData', []):
attack_montages.append({
'montageName': montage_data.get('assetName', 'N/A'),
'actualDuration': montage_data.get('actualDuration', 0)
})
summons[summon_name] = {
'summonSkillId': skill_id,
'summonSkillName': skill_data.get('name', ''),
'activeDuration': skill_data.get('activeDuration', 0),
'skillDamageRate': skill_data.get('skillDamageRate', 0), # 피해 배율 추가
'attackMontages': attack_montages,
'dotType': config.DOT_SKILLS.get(skill_id, {}).get('dot_type', '')
}
organized[stalker_id] = {
'id': stalker_id,
'stats': stats,
'abilities': abilities,
'basicAttacks': basic_attacks, # 평타 상세 정보
'skills': skills,
'defaultSkills': [skills.get(sid) for sid in stats['defaultSkills'] if sid in skills],
'subSkill': skills.get(stats['subSkill']),
'ultimateSkill': skills.get(stats['ultimateSkill']),
'summons': summons # 소환체 정보
}
skill_count = len(skills)
print(f" [OK] {stats['name']} ({stalker_id}): {skill_count}개 스킬")
return organized
def is_utility_skill(skill_data: Dict) -> bool:
"""
유틸리티 스킬 판별 (DPS 계산 제외 대상)
판별 기준:
1. config.UTILITY_SKILLS에 명시적으로 등록
2. skillAttackType == "Normal" AND skillDamageRate == 0
3. 몽타주에 공격 노티파이 없음 (montageData 확인)
예외: 소환 스킬은 항상 공격 스킬로 간주
"""
skill_id = skill_data['skillId']
# 소환 스킬은 공격 스킬 (유틸리티 아님)
if skill_data.get('isSummon', False):
return False
# 1. 수동 지정
if skill_id in config.UTILITY_SKILLS:
return True
# 2. Normal 타입 + Rate 0
if skill_data['skillAttackType'] == 'Normal' and skill_data['skillDamageRate'] == 0:
return True
# 3. 몽타주에 공격 노티파이 없음
montage_data_list = skill_data.get('montageData', [])
if montage_data_list:
has_attack = any(m.get('hasAttack', False) for m in montage_data_list)
if not has_attack and skill_data['skillDamageRate'] > 0:
# Rate는 있지만 공격 노티파이 없음 -> 유틸리티
return True
return False
def main():
"""메인 실행 함수"""
print("="*80)
print("스토커 데이터 통합 추출 v2")
print("="*80)
# 1. JSON 파일 로드
print("\n[ JSON 파일 로드 ]")
datatable_data = load_json(config.DATATABLE_JSON)
blueprint_data = load_json(config.BLUEPRINT_JSON)
animmontage_data = load_json(config.ANIMMONTAGE_JSON)
datatables = datatable_data.get('Assets', [])
blueprints = blueprint_data.get('Assets', [])
montages = animmontage_data.get('Assets', [])
# 2. 데이터 추출
stalker_stats = extract_character_stats(datatables)
stalker_abilities = extract_character_abilities(datatables)
all_skills = extract_skills(datatables)
skill_blueprints = extract_skill_blueprints(blueprints)
anim_montages = extract_anim_montages(montages)
npc_abilities = extract_npc_abilities(datatables) # 소환수 데이터
# 3. 데이터 통합
organized_data = organize_stalker_data(
stalker_stats,
stalker_abilities,
all_skills,
skill_blueprints,
anim_montages,
npc_abilities
)
# 4. 결과 저장 (새 디렉토리 생성)
output_dir = config.get_output_dir(create_new=True)
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / "intermediate_data.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(organized_data, f, ensure_ascii=False, indent=2)
print(f"\n[OK] 중간 데이터 저장 완료: {output_file}")
print(f" - 출력 디렉토리: {output_dir}")
print(f" - 총 {len(organized_data)}명 스토커 데이터")
return organized_data
if __name__ == "__main__":
main()

View File

@ -0,0 +1,571 @@
#!/usr/bin/env python3
"""
스토커 기본 데이터 문서 생성 스크립트 v2
validated_data.json (또는 intermediate_data.json)에서
03_스토커별_기본데이터_v2.md 생성
"""
import json
import sys
from pathlib import Path
from typing import Dict, List
from datetime import datetime
# config 임포트
sys.path.append(str(Path(__file__).parent))
import config
def generate_header() -> str:
"""문서 헤더 생성"""
return f"""# 03. 스토커별 기본 데이터 (v2)
## 데이터 소스
- `DT_CharacterStat`: 기본 스탯, 스킬 목록
- `DT_CharacterAbility`: 평타 몽타주
- `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 시전시간, 효과)
- `Blueprint`: 스킬 변수 (ActivationOrderGroup 등)
- `AnimMontage`: 타이밍 및 공격 노티파이, 실제 발사 시점
## 검증 상태
- ✅ 모든 데이터는 최신 JSON (2025-10-24 15:58:55)에서 추출
- ✅ 교차 검증 완료
- ✅ 출처 명시 (각 데이터 필드별)
## DPS 계산 시 고려사항
- **시전시간**: 스킬 사용 시 시전시간(CastingTime)이 추가됨
- **실제 공격 시점**: 원거리 스킬(우르드, 리안)의 경우 몽타주 시간보다 빠르게 공격 가능
- **DoT 데미지**: DoT(Damage over Time) 스킬은 대상 HP에 비례하여 지속 피해 발생 (구체적 계산은 다음 챕터 참조)
---
"""
def generate_stalker_overview(data: Dict) -> str:
"""10명 스토커 종합 비교표"""
md = "## 10명 스토커 종합 비교표\n\n"
md += "| 스토커 | 직업 | STR | DEX | INT | CON | WIS | 궁극기 | 장착 무기 | 평타 |\n"
md += "|--------|------|-----|-----|-----|-----|-----|--------|-----------|------|\n"
for stalker_id in config.STALKERS:
if stalker_id not in data:
continue
stalker = data[stalker_id]
stats = stalker['stats']
st = stats['stats']
# 궁극기
has_ultimate = "" if stats['ultimateSkill'] else ""
# 장착 무기
equip_types = ', '.join(stats['equipableTypes'])
# 평타 콤보
attack_map = stalker['abilities'].get('attackMontageMap', {})
combo_counts = []
for weapon_type, montage_data in attack_map.items():
count = len(montage_data.get('montageArray', []))
combo_counts.append(f"{count}")
combo_str = ', '.join(combo_counts) if combo_counts else "N/A"
md += f"| **{stats['name']}** | {stats['jobName']} | {st['str']} | {st['dex']} | {st['int']} | {st['con']} | {st['wis']} | {has_ultimate} | {equip_types} | {combo_str} |\n"
md += "\n**특징**:\n"
md += "- **모든 스토커가 궁극기 보유**\n"
md += "- 모든 스토커 스탯 합계: 75 포인트 (균형)\n"
md += "- HP/MP 동일: 100/50\n"
md += "- 마나 회복: 0.2/초 (전원 동일)\n\n"
md += "---\n\n"
return md
def generate_ultimate_overview(data: Dict) -> str:
"""궁극기 종합 비교"""
md = "## 궁극기 종합 비교\n\n"
md += "| 스토커 | 궁극기 이름 | 타입 | 피해배율 | 지속/시전 | 주요 효과 |\n"
md += "|--------|-------------|------|----------|-----------|----------|\n"
for stalker_id in config.STALKERS:
if stalker_id not in data:
continue
stalker = data[stalker_id]
ultimate_skill = stalker.get('ultimateSkill')
if not ultimate_skill:
continue
name = ultimate_skill.get('name', 'N/A')
skill_type = ultimate_skill.get('skillAttackType', 'Normal')
damage_rate = ultimate_skill.get('skillDamageRate', 0)
active_duration = ultimate_skill.get('activeDuration', 0)
casting_time = ultimate_skill.get('castingTime', 0)
# 설명 (descFormatted 사용 - 변수 치환 및 줄바꿈 제거됨)
desc = ultimate_skill.get('descFormatted', ultimate_skill.get('simpleDesc', ''))[:100]
stalker_name = stalker['stats']['name']
md += f"| **{stalker_name}** | {name} | {skill_type} | {damage_rate} | {active_duration}초 / {casting_time}초 | {desc}... |\n"
md += "\n---\n\n"
return md
def generate_dot_overview(data: Dict) -> str:
"""DoT 스킬 종합 비교"""
md = "## DoT 스킬 종합 비교\n\n"
md += "다음 스킬들은 DoT(Damage over Time) 효과가 있으며, **DPS 계산 시 추가 지속 피해를 고려해야 합니다**.\n\n"
md += "| 스토커 | 스킬 이름 | DoT 타입 | 기본 피해 | DoT 피해 | 지속시간 |\n"
md += "|--------|----------|----------|----------|----------|----------|\n"
# config.DOT_SKILLS에서 DoT 스킬 정보 가져오기
for skill_id, dot_info in config.DOT_SKILLS.items():
stalker_id = dot_info['stalker']
if stalker_id not in data:
continue
stalker = data[stalker_id]
stalker_name = stalker['stats']['name']
skills = stalker.get('skills', {})
if skill_id not in skills:
continue
skill = skills[skill_id]
skill_name = skill.get('name', 'N/A')
dot_type = dot_info.get('dot_type', 'DoT')
damage_rate = skill.get('skillDamageRate', 0)
# DoT 피해 설명
if dot_type == 'Poison':
dot_damage = "대상 MaxHP의 20%"
duration = "5초"
elif dot_type == 'Burn':
dot_damage = "대상 MaxHP의 10%"
duration = "3초"
elif dot_type == 'Bleed':
dot_damage = "고정 20 피해"
duration = "5초"
else:
dot_damage = "N/A"
duration = "N/A"
md += f"| **{stalker_name}** | {skill_name} | {dot_type} | {damage_rate} | {dot_damage} | {duration} |\n"
md += "\n**주의사항**:\n"
md += "- DoT 피해는 대상의 HP에 비례하므로, 적의 체력에 따라 실제 피해량이 달라집니다.\n"
md += "- 구체적인 DoT DPS 계산 방법은 다음 챕터에서 다룹니다.\n"
md += "- 위 표의 '기본 피해'는 스킬의 skillDamageRate입니다.\n\n"
md += "---\n\n"
return md
def get_montage_tag(montage_name: str) -> str:
"""
몽타주 이름에서 태그 추출
Args:
montage_name: 몽타주 이름
Returns:
태그 문자열 (예: "[준비]", "[장비]") 또는 빈 문자열
"""
montage_tags = config.SEQUENCE_CALCULATION_RULES.get('montage_tags', {})
exclude_keywords = config.SEQUENCE_CALCULATION_RULES.get('exclude_keywords', [])
for keyword in exclude_keywords:
if keyword.lower() in montage_name.lower():
return montage_tags.get(keyword, '')
return ''
def calculate_sequence_length(skill_id: str, montage_data: List[Dict]) -> tuple:
"""
스킬의 시퀀스 길이 계산
Args:
skill_id: 스킬 ID
montage_data: 몽타주 데이터 리스트
Returns:
(sequence_length, is_average, included_montages)
- sequence_length: 계산된 시퀀스 길이
- is_average: 평균 계산 여부
- included_montages: 계산에 포함된 몽타주 리스트 (인덱스)
"""
if not montage_data:
return 0, False, []
rules = config.SEQUENCE_CALCULATION_RULES
exclude_keywords = rules.get('exclude_keywords', [])
average_skills = rules.get('average_skills', [])
exclude_montages = rules.get('exclude_montages', {})
exclude_montage_indices = rules.get('exclude_montage_indices', {})
# 1. 특정 몽타주 제외 리스트 가져오기
skill_exclude_list = exclude_montages.get(skill_id, [])
skill_exclude_indices = exclude_montage_indices.get(skill_id, [])
# 2. 포함될 몽타주 필터링
included_montages = []
for idx, montage in enumerate(montage_data):
montage_name = montage.get('assetName', '')
# 인덱스로 제외 체크
if idx in skill_exclude_indices:
continue
# 특정 몽타주 제외 체크
if montage_name in skill_exclude_list:
continue
# 키워드 제외 체크 (대소문자 구분 없음)
has_exclude_keyword = any(
keyword.lower() in montage_name.lower()
for keyword in exclude_keywords
)
if not has_exclude_keyword:
included_montages.append(idx)
# 3. 포함된 몽타주가 없으면 0 반환
if not included_montages:
return 0, False, []
# 4. 시퀀스 길이 계산
is_average = skill_id in average_skills
if is_average:
# 평균 계산
total = sum(montage_data[idx].get('actualDuration', 0) for idx in included_montages)
sequence_length = total / len(included_montages) if included_montages else 0
else:
# 합산 계산
sequence_length = sum(montage_data[idx].get('actualDuration', 0) for idx in included_montages)
return sequence_length, is_average, included_montages
def generate_stalker_detail(stalker_id: str, stalker_data: Dict) -> str:
"""개별 스토커 상세 정보"""
stats = stalker_data['stats']
st = stats['stats']
info = config.STALKER_INFO.get(stalker_id, {})
# stats['name']은 이미 "English (Korean)" 형식
md = f"## {config.STALKERS.index(stalker_id) + 1}. {stats['name']} - {info.get('role', stats['jobName'])}\n\n"
# 기본 정보
md += "### 기본 정보\n"
md += f"- **역할**: {info.get('role', 'N/A')}\n"
md += f"- **주 스탯**: "
# 주 스탯 찾기 (가장 높은 2개)
stat_pairs = [(k.upper(), v) for k, v in st.items()]
stat_pairs.sort(key=lambda x: x[1], reverse=True)
md += f"{stat_pairs[0][0]} {stat_pairs[0][1]}, {stat_pairs[1][0]} {stat_pairs[1][1]}\n"
md += f"- **HP**: {stats['hp']} | **MP**: {stats['mp']} | **마나 회복**: {stats['manaRegen']}/초\n"
# 크리티컬 스탯
crit_per = stats.get('criticalPer', 5)
crit_dmg = stats.get('criticalDamage', 0)
md += f"- **크리티컬**: 확률 {crit_per}% | 추가 피해 {crit_dmg}%\n"
# 장착 무기
equip_types = ', '.join(stats['equipableTypes'])
md += f"- **장착 가능**: {equip_types}\n"
# 평타
attack_map = stalker_data['abilities'].get('attackMontageMap', {})
if attack_map:
combo_info = []
for weapon_type, montage_data in attack_map.items():
count = len(montage_data.get('montageArray', []))
combo_info.append(f"{weapon_type} {count}")
md += f"- **평타**: {', '.join(combo_info)}\n"
md += "\n"
# 평타 상세 정보
basic_attacks = stalker_data.get('basicAttacks', {})
if basic_attacks:
md += "### 평타 상세 정보\n\n"
for weapon_type, attacks in basic_attacks.items():
if attacks:
md += f"**{weapon_type}** ({len(attacks)}타 콤보):\n\n"
md += "| 타수 | 몽타주 | 시간(초) | 배율(%) | 비고 |\n"
md += "|------|--------|----------|---------|------|\n"
for attack in attacks:
idx = attack['index']
montage_name = attack['montageName']
duration = attack['actualDuration']
multiplier = attack['attackMultiplier']
mult_display = f"{multiplier:+.1f}" if multiplier != 0 else "0.0"
# 태그 추가
tag = get_montage_tag(montage_name)
note = tag if tag else ""
md += f"| {idx} | {montage_name} | {duration:.2f} | {mult_display} | {note} |\n"
md += "\n"
# 기본 스킬
md += "### 스킬 목록\n\n"
md += "**기본 스킬**:\n\n"
default_skills = stalker_data.get('defaultSkills', [])
for idx, skill in enumerate(default_skills, 1):
if not skill:
continue
md += generate_skill_entry(skill, idx)
# 서브 스킬
sub_skill = stalker_data.get('subSkill')
if sub_skill:
md += "\n**서브 스킬**:\n\n"
md += generate_skill_entry(sub_skill, 0, is_sub=True)
# 궁극기
ultimate_skill = stalker_data.get('ultimateSkill')
if ultimate_skill:
md += "\n**궁극기**:\n\n"
md += generate_skill_entry(ultimate_skill, 0, is_ultimate=True)
# 소환체 (레네만)
summons = stalker_data.get('summons', {})
if summons:
md += "\n### 소환체\n\n"
for summon_name, summon_data in summons.items():
md += generate_summon_entry(summon_name, summon_data)
md += "\n---\n\n"
return md
def generate_summon_entry(summon_name: str, summon_data: Dict) -> str:
"""소환체 엔트리 생성"""
summon_skill_id = summon_data.get('summonSkillId', 'N/A')
summon_skill_name = summon_data.get('summonSkillName', 'N/A')
active_duration = summon_data.get('activeDuration', 0)
skill_damage_rate = summon_data.get('skillDamageRate', 0)
attack_montages = summon_data.get('attackMontages', [])
dot_type = summon_data.get('dotType', '')
# 소환체 타입별 아이콘
icon = ''
if 'ifrit' in summon_name.lower() or '화염' in summon_skill_name:
icon = '🔥'
elif 'shiva' in summon_name.lower() or '냉기' in summon_skill_name or '얼음' in summon_skill_name:
icon = '❄️'
md = f"#### {icon} {summon_name}\n\n"
md += f"- **소환 스킬**: {summon_skill_id} {summon_skill_name}\n"
if active_duration > 0:
md += f"- **소환 유지 시간**: {active_duration}\n"
# 공격 몽타주 정보 및 DPS 계산
if attack_montages:
md += f"- **공격 몽타주**: \n"
# 공격 사이클 계산 (순차적 반복)
total_cycle_time = 0
montage_durations = []
for montage in attack_montages:
montage_name = montage.get('montageName', 'N/A')
duration = montage.get('actualDuration', 0)
md += f" - {montage_name} ({duration:.2f}초)\n"
total_cycle_time += duration
montage_durations.append(duration)
# 공격 사이클 및 DPS 계산
if len(attack_montages) > 0 and total_cycle_time > 0:
# 공격 사이클 표시
if len(attack_montages) == 1:
# 몽타주 1개
md += f"- **공격 사이클**: {montage_durations[0]:.2f}초 (반복)\n"
else:
# 몽타주 2개 이상: 순차 표시 + 총 합계
cycle_str = "".join([f"{d:.2f}" for d in montage_durations])
md += f"- **공격 사이클**: {cycle_str} (총 {total_cycle_time:.2f}초, 반복)\n"
# 예상 공격 횟수 계산
if active_duration > 0:
cycle_count = active_duration / total_cycle_time
attack_count = cycle_count * len(attack_montages)
total_damage = attack_count * skill_damage_rate
md += f"- **예상 공격 횟수**: ~{attack_count:.1f}\n"
md += f"- **총 피해 배율**: ~{total_damage:.2f}배 상당\n"
if dot_type:
dot_config = config.DOT_DAMAGE.get(dot_type, {})
dot_desc = dot_config.get('description', f'{dot_type} DoT')
md += f"- **특수 효과**: {dot_type} DoT ({dot_desc})\n"
md += "\n"
return md
def generate_skill_entry(skill: Dict, index: int, is_sub: bool = False, is_ultimate: bool = False) -> str:
"""개별 스킬 엔트리 생성"""
skill_id = skill.get('skillId', 'N/A')
name = skill.get('name', 'N/A')
skill_type = skill.get('skillAttackType', 'Normal')
element = skill.get('skillElementType', 'None')
damage_rate = skill.get('skillDamageRate', 0)
cooltime = skill.get('coolTime', 0)
mana = skill.get('manaCost', 0)
casting_time = skill.get('castingTime', 0)
# 설명 (descFormatted 사용 - 변수 치환 및 줄바꿈 제거됨)
desc = skill.get('descFormatted', skill.get('simpleDesc', ''))
md = ""
if index > 0:
md += f"{index}. "
md += f"**{skill_id} {name}**\n"
md += f" - **타입**: {skill_type}"
if element and element != 'None':
md += f" / **속성**: {element}"
md += "\n"
if damage_rate > 0:
md += f" - **피해 배율**: {damage_rate}\n"
# 쿨타임, 마나, 시전시간 표시
if cooltime > 0 or mana > 0 or casting_time > 0:
parts = []
if cooltime > 0:
parts.append(f"**쿨타임**: {cooltime}")
if mana > 0:
parts.append(f"**마나**: {mana}")
if casting_time > 0:
parts.append(f"**시전시간**: {casting_time}")
if parts:
md += f" - {' / '.join(parts)}\n"
# 특수 마커
is_dot = skill.get('isDot', False)
is_summon = skill.get('isSummon', False)
is_utility = skill.get('isUtility', False)
# 유틸리티 스킬 표시
if is_utility:
md += f" - 💡 **유틸리티 스킬** (DPS 계산 제외)\n"
if is_dot:
dot_info = config.DOT_SKILLS.get(skill_id, {})
dot_type = dot_info.get('dot_type', 'DoT')
# DoT 피해 상세 정보
if dot_type == 'Poison':
dot_detail = "대상 MaxHP의 20% (5초간)"
elif dot_type == 'Burn':
dot_detail = "대상 MaxHP의 10% (3초간)"
elif dot_type == 'Bleed':
dot_detail = "고정 20 피해 (5초간)"
else:
dot_detail = "지속 피해"
md += f" - ⚠️ **{dot_type} 상태이상 유발**: {dot_detail}\n"
md += f" - 💡 **DoT 피해는 대상 HP에 비례** (구체적 DPS는 다음 챕터 참조)\n"
if is_summon:
summon_info = config.SUMMON_SKILLS.get(skill_id, {})
summon_name = summon_info.get('summon', 'Summon')
duration = skill.get('activeDuration', 0)
md += f" - 🔮 **소환**: {summon_name} (지속 {duration}초)\n"
# 몽타주 정보 표시 (이름 + 시간 + 태그)
montage_data = skill.get('montageData', [])
if montage_data:
if len(montage_data) == 1:
# 몽타주 1개: 한 줄로 표시
montage = montage_data[0]
montage_name = montage.get('assetName', 'N/A')
tag = get_montage_tag(montage_name)
tag_display = f" {tag}" if tag else ""
md += f" - **몽타주**: {montage_name}{tag_display}\n"
else:
# 몽타주 여러 개: 리스트로 표시
md += f" - **몽타주**: \n"
for idx, montage in enumerate(montage_data, 1):
montage_name = montage.get('assetName', 'N/A')
duration = montage.get('actualDuration', 0)
tag = get_montage_tag(montage_name)
tag_display = f" {tag}" if tag else ""
md += f" {idx}. {montage_name} ({duration:.2f}초){tag_display}\n"
# 시퀀스 길이 (새로운 계산 규칙 적용)
sequence_length, is_average, included_montages = calculate_sequence_length(skill_id, montage_data)
if sequence_length > 0 or len(montage_data) > 0:
# 평균 표시 추가
avg_text = " (평균)" if is_average else ""
md += f" - **시퀀스 길이**: {sequence_length:.2f}{avg_text}\n"
# 설명 (전체 표시)
if desc:
md += f" - **설명**: {desc}\n"
md += "\n"
return md
def main():
"""메인 실행 함수"""
print("="*80)
print("스토커 기본 데이터 문서 생성 v2")
print("="*80)
# 검증된 데이터 로드 (없으면 intermediate 사용)
validated_file = config.OUTPUT_DIR / "validated_data.json"
intermediate_file = config.OUTPUT_DIR / "intermediate_data.json"
if validated_file.exists():
data_file = validated_file
print(f"\n[ 검증된 데이터 사용 ]: {data_file}")
elif intermediate_file.exists():
data_file = intermediate_file
print(f"\n[ 중간 데이터 사용 ]: {data_file}")
print("⚠️ 검증되지 않은 데이터입니다. validate_stalker_data.py를 먼저 실행하는 것을 권장합니다.")
else:
print(f"[FAIL] 데이터 파일 없음")
print("먼저 extract_stalker_data_v2.py를 실행하세요.")
return
with open(data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
print("\n[ 문서 생성 시작 ]")
# 마크다운 생성
md_content = generate_header()
md_content += generate_stalker_overview(data)
md_content += generate_ultimate_overview(data)
md_content += generate_dot_overview(data) # DoT 스킬 종합
# 개별 스토커
for stalker_id in config.STALKERS:
if stalker_id not in data:
print(f"[WARN] {stalker_id}: 데이터 없음, 건너뜀")
continue
print(f" - {stalker_id} 문서 생성 중...")
md_content += generate_stalker_detail(stalker_id, data[stalker_id])
# Footer
md_content += "---\n\n"
md_content += f"**생성 일시**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
md_content += f"**데이터 소스**: {data_file.name}\n"
md_content += f"**검증 상태**: {'검증 완료 ✅' if data_file.name == 'validated_data.json' else '미검증 ⚠️'}\n"
# 파일 저장
output_file = config.OUTPUT_DIR / "03_스토커별_기본데이터_v2.md"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(md_content)
print(f"\n[OK] 문서 생성 완료: {output_file}")
print(f" - 총 {len(data)}명 스토커 문서 생성")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,107 @@
# 유틸리티 스크립트
JSON 데이터 탐색 및 정보 조회를 위한 유틸리티 도구 모음
---
## 📋 스크립트 목록
### list_asset_types.py
**용도**: AnimMontage.json의 모든 Asset 타입 목록 출력
**사용 예시**:
```bash
python utils/list_asset_types.py
```
**출력**:
```
총 743개 Asset 발견
Asset 타입 분포:
- AnimMontage: 743개
```
**활용 사례**:
- AnimMontage.json에 어떤 타입의 Asset이 있는지 확인
- 새로운 데이터 소스 추가 시 구조 파악
---
### list_datatables.py
**용도**: DataTable.json의 모든 DataTable Asset 목록 출력
**사용 예시**:
```bash
python utils/list_datatables.py
```
**출력**:
```
총 23개 DataTable 발견:
1. DT_CharacterStat (10 rows)
2. DT_CharacterAbility (10 rows)
3. DT_Skill (91 rows)
4. DT_NPCAbility (15 rows)
...
```
**활용 사례**:
- DataTable.json에 어떤 테이블이 있는지 확인
- 새로운 DataTable 추가 시 Row 개수 확인
- 데이터 구조 탐색
---
## 🔧 개발자 가이드
### 새로운 유틸리티 추가 방법
1. **파일 생성**: `utils/` 폴더에 새 스크립트 생성
2. **명명 규칙**: `{동사}_{대상}.py` (예: `extract_skill_names.py`)
3. **공통 패턴 준수**:
```python
#!/usr/bin/env python3
"""
[스크립트 용도 설명]
"""
import json
from pathlib import Path
# 프로젝트 루트
PROJECT_ROOT = Path(__file__).parent.parent.parent
DATA_DIR = PROJECT_ROOT / "원본데이터"
def main():
"""메인 실행 함수"""
# JSON 로드
with open(DATA_DIR / "DataTable.json", 'r', encoding='utf-8') as f:
data = json.load(f)
# 로직 구현
# ...
# 결과 출력
print(f"결과: ...")
if __name__ == "__main__":
main()
```
4. **README 업데이트**: 이 파일에 새 스크립트 설명 추가
---
## 📚 관련 파일
- **../config.py** - 프로젝트 전역 설정
- **../../원본데이터/** - JSON 데이터 소스
- **../../ARCHITECTURE.md** - 데이터 구조 상세 문서
---
**작성자**: AI-assisted Development Team
**최종 업데이트**: 2025-10-27

View File

@ -0,0 +1,25 @@
"""모든 Asset Type 확인"""
import json
from collections import Counter
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
print(f"총 Assets: {len(assets)}\n")
# Type 카운트
types = [a.get('Type', 'Unknown') for a in assets]
type_counts = Counter(types)
print("Asset Type 분포:")
for asset_type, count in type_counts.most_common():
print(f" - {asset_type}: {count}")
# 이름 샘플 (각 타입별 3개씩)
print("\n\nType별 이름 샘플:")
for asset_type, count in type_counts.most_common():
print(f"\n{asset_type} ({count}개):")
samples = [a.get('Name', '') for a in assets if a.get('Type') == asset_type][:5]
for sample in samples:
print(f" - {sample}")

View File

@ -0,0 +1,19 @@
"""모든 DataTable 나열"""
import json
with open('../../원본데이터/DataTable.json', 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
print(f"총 Assets: {len(assets)}\n")
# DataTable만 필터링
datatables = [a for a in assets if a.get('Type') == 'DataTable']
print(f"DataTable 개수: {len(datatables)}\n")
# 이름 출력
print("DataTable 목록:")
for dt in datatables:
name = dt.get('Name', '')
rows_count = len(dt.get('Rows', {}))
print(f" - {name} ({rows_count} rows)")

View File

@ -0,0 +1,356 @@
#!/usr/bin/env python3
"""
스토커 데이터 검증 스크립트 v2
intermediate_data.json의 데이터 정확성을 교차 검증
"""
import json
import sys
from pathlib import Path
from typing import Dict, List, Tuple
# config 임포트
sys.path.append(str(Path(__file__).parent))
import config
class ValidationReport:
"""검증 리포트"""
def __init__(self):
self.passes = []
self.warnings = []
self.failures = []
def add_pass(self, message: str):
self.passes.append(f"[PASS] {message}")
def add_warning(self, message: str):
self.warnings.append(f"[WARN] {message}")
def add_failure(self, message: str):
self.failures.append(f"[FAIL] {message}")
def print_summary(self):
"""요약 출력"""
print("\n" + "="*80)
print("검증 리포트 요약")
print("="*80)
print(f"[PASS] 통과: {len(self.passes)}")
print(f"[WARN] 경고: {len(self.warnings)}")
print(f"[FAIL] 실패: {len(self.failures)}")
total = len(self.passes) + len(self.warnings) + len(self.failures)
if total > 0:
confidence = (len(self.passes) / total) * 100
print(f"[INFO] 데이터 신뢰도: {confidence:.1f}%")
def print_details(self):
"""상세 출력"""
if self.failures:
print("\n[ 실패 항목 ]")
for fail in self.failures:
print(fail)
if self.warnings:
print("\n[ 경고 항목 ]")
for warn in self.warnings:
print(warn)
if self.passes and len(self.passes) <= 20:
print("\n[ 통과 항목 (샘플) ]")
for pass_msg in self.passes[:10]:
print(pass_msg)
def to_markdown(self, output_path: Path):
"""마크다운 파일로 저장"""
with open(output_path, 'w', encoding='utf-8') as f:
f.write("# 데이터 검증 리포트\n\n")
f.write(f"**생성 시각**: {Path(output_path).stat().st_mtime}\n\n")
f.write("## 전체 요약\n\n")
f.write(f"- ✅ 검증 통과: **{len(self.passes)}개** 항목\n")
f.write(f"- ⚠️ 경고: **{len(self.warnings)}개** 항목\n")
f.write(f"- ❌ 실패: **{len(self.failures)}개** 항목\n")
total = len(self.passes) + len(self.warnings) + len(self.failures)
if total > 0:
confidence = (len(self.passes) / total) * 100
f.write(f"- 📊 데이터 신뢰도: **{confidence:.1f}%**\n\n")
if self.failures:
f.write("## ❌ 실패 항목\n\n")
for fail in self.failures:
f.write(f"{fail}\n\n")
if self.warnings:
f.write("## ⚠️ 경고 항목\n\n")
for warn in self.warnings:
f.write(f"{warn}\n\n")
f.write("## ✅ 통과 항목\n\n")
f.write(f"{len(self.passes)}개 항목이 검증을 통과했습니다.\n\n")
def validate_stalker_count(data: Dict, report: ValidationReport):
"""스토커 수 검증"""
expected_count = len(config.STALKERS)
actual_count = len(data)
if actual_count == expected_count:
report.add_pass(f"스토커 수 일치 ({actual_count}명)")
else:
report.add_failure(f"스토커 수 불일치 (예상:{expected_count}, 실제:{actual_count})")
def validate_stalker_stats(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""스토커 기본 스탯 검증"""
stats = stalker_data['stats']['stats']
# 스탯 합계
stat_sum = sum(stats.values())
expected_sum = config.VALIDATION_RULES['stat_total']
if stat_sum == expected_sum:
report.add_pass(f"{stalker_id}: 스탯 합계 = {stat_sum}")
else:
report.add_failure(f"{stalker_id}: 스탯 합계 불일치 (예상:{expected_sum}, 실제:{stat_sum})")
# HP/MP 검증
hp = stalker_data['stats']['hp']
mp = stalker_data['stats']['mp']
if hp == config.VALIDATION_RULES['hp']:
report.add_pass(f"{stalker_id}: HP = {hp}")
else:
report.add_warning(f"{stalker_id}: HP 불일치 (예상:{config.VALIDATION_RULES['hp']}, 실제:{hp})")
if mp == config.VALIDATION_RULES['mp']:
report.add_pass(f"{stalker_id}: MP = {mp}")
else:
report.add_warning(f"{stalker_id}: MP 불일치 (예상:{config.VALIDATION_RULES['mp']}, 실제:{mp})")
def validate_skills(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""스킬 데이터 검증"""
skills = stalker_data['skills']
for skill_id, skill_data in skills.items():
# skillDamageRate 범위
rate = skill_data.get('skillDamageRate', 0)
if rate < 0:
report.add_failure(f"{stalker_id}/{skill_id}: skillDamageRate = {rate} (음수)")
# coolTime 범위
cooltime = skill_data.get('coolTime', 0)
if cooltime < 0:
report.add_failure(f"{stalker_id}/{skill_id}: coolTime = {cooltime} (음수)")
# 궁극기 체크
if skill_data.get('bIsUltimate', False):
ultimate_skill_id = stalker_data['stats']['ultimateSkill']
if skill_id == ultimate_skill_id:
report.add_pass(f"{stalker_id}: 궁극기 매칭 ({skill_id})")
else:
report.add_warning(f"{stalker_id}: 궁극기 ID 불일치 (스탯:{ultimate_skill_id}, 스킬:{skill_id})")
# 몽타주 연결 검증
use_montages = skill_data.get('useMontages', [])
montage_data = skill_data.get('montageData', [])
if len(use_montages) > 0 and len(montage_data) == 0:
report.add_warning(f"{stalker_id}/{skill_id}: 몽타주 경로는 있지만 데이터 없음")
# 유틸리티 스킬 판별 검증
is_utility = skill_data.get('isUtility', False)
has_attack = any(m.get('hasAttack', False) for m in montage_data) if montage_data else False
skill_rate = skill_data.get('skillDamageRate', 0)
if is_utility and has_attack and skill_rate > 0:
report.add_warning(f"{stalker_id}/{skill_id}: 유틸리티로 분류되었지만 공격 노티파이 있음")
def validate_summon_skills(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""소환수 스킬 검증"""
if stalker_id != 'rene':
return
skills = stalker_data['skills']
for skill_id in config.SUMMON_SKILLS.keys():
if skill_id not in skills:
report.add_failure(f"{stalker_id}: 소환수 스킬 {skill_id} 없음")
continue
skill_data = skills[skill_id]
# activeDuration 확인
duration = skill_data.get('activeDuration', 0)
if duration == 0:
report.add_failure(f"{stalker_id}/{skill_id}: 소환 지속시간 = 0")
else:
report.add_pass(f"{stalker_id}/{skill_id}: 소환 지속시간 = {duration}")
# 시전 몽타주 확인 (공격 노티파이 체크 제외 - 소환만 하기 때문)
montage_data = skill_data.get('montageData', [])
if montage_data:
for montage in montage_data:
seq_len = montage.get('sequenceLength', 0)
montage_name = montage.get('assetName', '')
if seq_len == 0:
report.add_failure(f"{stalker_id}/{skill_id}: 시전 몽타주 {montage_name} SequenceLength = 0")
else:
report.add_pass(f"{stalker_id}/{skill_id}: 시전 몽타주 {montage_name} = {seq_len:.2f}")
# 소환수 공격 몽타주 확인
summon_montage_data = skill_data.get('summonMontageData', [])
summon_type = skill_data.get('summonType', 'npc')
if not summon_montage_data:
report.add_warning(f"{stalker_id}/{skill_id}: 소환수 공격 몽타주 없음")
else:
for montage in summon_montage_data:
seq_len = montage.get('sequenceLength', 0)
has_attack = montage.get('hasAttack', False)
montage_name = montage.get('assetName', '')
attack_interval = montage.get('attackInterval', 0)
if seq_len == 0:
report.add_failure(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} SequenceLength = 0")
else:
if summon_type == 'special' and attack_interval > 0:
# Shiva: 공격 주기 표시
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} (주기: {attack_interval:.2f}초)")
else:
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} = {seq_len:.2f}")
# 소환수 공격 노티파이 확인
# NPC 소환수는 AnimNotify 외의 방식으로 피해를 입힐 수 있으므로 PASS 처리
if has_attack:
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} 공격 노티파이 확인")
else:
# 공격 노티파이 없어도 정상 (소환수는 다른 방식으로 피해 입힘)
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} (AnimNotify 방식 아님)")
def validate_dot_skills(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""DoT 스킬 검증"""
skills = stalker_data['skills']
for skill_id, dot_info in config.DOT_SKILLS.items():
if dot_info['stalker'] != stalker_id:
continue
if skill_id not in skills:
report.add_failure(f"{stalker_id}: DoT 스킬 {skill_id} 없음")
continue
skill_data = skills[skill_id]
is_dot = skill_data.get('isDot', False)
if is_dot:
report.add_pass(f"{stalker_id}/{skill_id}: DoT 스킬 마킹 확인")
else:
report.add_warning(f"{stalker_id}/{skill_id}: DoT 스킬이지만 마킹 안 됨")
def validate_blueprint_connections(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""Blueprint 연결 검증"""
skills = stalker_data['skills']
# 재장전 스킬은 Blueprint 변수 없어도 정상 (상수 사용)
reload_skills = ['SK110207', 'SK190209'] # urud, lian 재장전
ultimate_exceptions = ['SK170301'] # cazimord 궁극기
for skill_id, skill_data in skills.items():
ability_class = skill_data.get('abilityClass', '')
if not ability_class or ability_class == 'None':
continue
bp_vars = skill_data.get('blueprintVariables', {})
bp_name = ability_class.split('/')[-1].split('.')[0] if '/' in ability_class else ability_class
if not bp_vars:
# 재장전 스킬은 경고 제외
if skill_id in reload_skills:
report.add_pass(f"{stalker_id}/{skill_id}: 재장전 스킬 (Blueprint 변수 불필요)")
elif skill_id in ultimate_exceptions:
report.add_pass(f"{stalker_id}/{skill_id}: Blueprint 변수 없음 (정상 - {bp_name})")
else:
report.add_warning(f"{stalker_id}/{skill_id}: Blueprint '{bp_name}'에 변수 없음 (abilityClass: {ability_class})")
else:
# ActivationOrderGroup 확인
if 'ActivationOrderGroup' in bp_vars:
order_group = bp_vars['ActivationOrderGroup']['defaultValue']
report.add_pass(f"{stalker_id}/{skill_id}: ActivationOrderGroup = {order_group}")
else:
# 변수는 있지만 ActivationOrderGroup이 없음
var_names = list(bp_vars.keys())[:3] # 처음 3개 변수명만
report.add_pass(f"{stalker_id}/{skill_id}: Blueprint 변수 {len(bp_vars)}개 (예: {', '.join(var_names)})")
def main():
"""메인 실행 함수"""
print("="*80)
print("스토커 데이터 검증 v2")
print("="*80)
# 중간 데이터 로드
intermediate_file = config.OUTPUT_DIR / "intermediate_data.json"
if not intermediate_file.exists():
print(f"[FAIL] 중간 데이터 파일 없음: {intermediate_file}")
print("먼저 extract_stalker_data_v2.py를 실행하세요.")
return
print(f"\n[ 중간 데이터 로드 ]: {intermediate_file}")
with open(intermediate_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# 검증 실행
report = ValidationReport()
print("\n[ 검증 시작 ]")
# 1. 전체 스토커 수
print("\n1. 스토커 수 검증...")
validate_stalker_count(data, report)
# 2. 스토커별 검증
for stalker_id in config.STALKERS:
if stalker_id not in data:
report.add_failure(f"{stalker_id}: 데이터 없음")
continue
stalker_data = data[stalker_id]
print(f"\n2. {stalker_id} 검증...")
# 기본 스탯
validate_stalker_stats(stalker_id, stalker_data, report)
# 스킬
validate_skills(stalker_id, stalker_data, report)
# 소환수 (레네만)
validate_summon_skills(stalker_id, stalker_data, report)
# DoT
validate_dot_skills(stalker_id, stalker_data, report)
# Blueprint 연결
validate_blueprint_connections(stalker_id, stalker_data, report)
# 3. 결과 출력
report.print_summary()
report.print_details()
# 4. 마크다운 저장
report_file = config.OUTPUT_DIR / "검증_리포트.md"
report.to_markdown(report_file)
print(f"\n[OK] 검증 리포트 저장: {report_file}")
# 5. 검증 데이터 저장 (통과한 데이터만)
if len(report.failures) == 0:
validated_file = config.OUTPUT_DIR / "validated_data.json"
with open(validated_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"[OK] 검증된 데이터 저장: {validated_file}")
else:
print(f"\n[WARN] 실패 항목이 있어 validated_data.json을 생성하지 않았습니다.")
print(f" intermediate_data.json은 그대로 유지됩니다.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,204 @@
# 장기 과제: Blueprint 변수 검증
## 과제 개요
DT_Skill의 스킬 설명(desc)에 사용된 {0}, {1} 등의 변수가 실제 게임 로직(Blueprint, 몽타주 등)의 값과 일치하는지 검증하는 작업
## 현재 상태
### ✅ 완료된 부분
- DT_Skill의 `desc` (원본 설명 문자열) 추출
- DT_Skill의 `descValues` (UI 표시용 값 배열) 추출
- `descFormatted` (변수 치환된 최종 설명) 생성
- 문서에 완전한 스킬 설명 표시
### ⚠️ 제한 사항
**descValues는 유저에게 보여주기 위한 텍스트 정보일 뿐, 실제 게임 로직에서는 사용되지 않음**
- 실제 스킬 효과는 Blueprint, AnimMontage, GameplayEffect 등에 정의됨
- descValues와 실제 게임 로직 값이 다를 가능성 존재
- 각 변수가 어느 Blueprint/몽타주의 어떤 변수와 연결되는지 case-by-case 분석 필요
## 검증 대상 예시
### 예시 1: Hilda 반격 (SK100202)
**DT_Skill 정보**:
```
desc: "방패를 들어 {0}초 동안 반격 자세를 취합니다. 반격 성공 시 {1}%만큼 물리 피해를 줍니다."
descValues: [5, 80]
abilityClass: /Game/Blueprints/Abilities/GA_Skill_Knight_Counter.GA_Skill_Knight_Counter_C
```
**검증 필요 사항**:
- {0} = 5초 → GA_Skill_Knight_Counter의 어느 변수? (activeDuration? blockingDuration?)
- {1} = 80% → GA_Skill_Knight_Counter의 어느 변수? (counterDamageMultiplier?)
**현재 문제**:
- Blueprint.json에서 GA_Skill_Knight_Counter 추출 시 모든 VarName이 `None`으로 나타남
- 변수 이름 없이는 매칭 불가능
### 예시 2: Urud 다발 화살 (SK110205)
**DT_Skill 정보**:
```
desc: "{0}발의 화살을 동시에 발사하여 각각 {1}%만큼 물리 피해를 입힙니다."
descValues: [3, 90]
abilityClass: /Game/Blueprints/Characters/Urud/GA_Skill_Urud_MultiShot_Quick.GA_Skill_Urud_MultiShot_Quick_C
```
**검증 필요 사항**:
- {0} = 3발 → Blueprint의 ProjectileCount? 또는 몽타주의 AN_Trigger_Projectile_Shot_C 호출 횟수?
- {1} = 90% → 이미 DT_Skill.skillDamageRate=0.9로 검증됨 ✅
## 작업 범위
### 1단계: 정보 수집 (필수)
Blueprint 변수 이름을 얻기 위한 방법 선택:
#### 옵션 A: Blueprint.json 재추출
- FModel 또는 다른 추출 도구 설정 변경
- VarName 필드가 포함되도록 추출 옵션 조정
- 또는 Unreal Editor에서 직접 Blueprint을 JSON으로 Export
#### 옵션 B: 수동 조사
- Unreal Editor에서 각 GA_Skill Blueprint 열기
- 변수 목록과 기본값을 수동으로 기록
- config.py에 수동으로 정의
#### 옵션 C: 코드 분석
- C++ 소스 코드에서 WSGameplayAbility 클래스 분석
- 각 GA_Skill의 부모 클래스 변수 확인
- .h/.cpp 파일에서 변수 정의 추출
### 2단계: 변수 매칭 규칙 정의
각 스킬 타입별 변수 매칭 패턴 정의:
```python
# 예시 매칭 규칙
SKILL_VAR_MAPPING = {
'counter_skills': {
# 반격 스킬 계열
'{0}': 'blockingDuration', # 반격 지속 시간
'{1}': 'counterDamageRate' # 반격 피해 배율
},
'projectile_skills': {
# 발사체 스킬 계열
'{0}': 'projectileCount', # 발사체 개수
'{1}': 'skillDamageRate' # 피해 배율 (DT_Skill에서 검증 가능)
},
'summon_skills': {
# 소환 스킬 계열
'{0}': 'activeDuration', # 소환 지속 시간 (DT_Skill에서 검증 가능)
'{1}': 'summonDamageRate' # 소환수 피해 배율
}
}
```
### 3단계: 자동 검증 스크립트 작성
```python
def verify_skill_desc_values(skill_id: str, skill_data: Dict) -> Dict:
"""
스킬 설명의 변수가 실제 Blueprint/몽타주 값과 일치하는지 검증
Returns:
{
'verified': bool,
'mismatches': [],
'sources': {
'{0}': {'expected': 5, 'actual': 5, 'source': 'Blueprint.activeDuration'},
'{1}': {'expected': 80, 'actual': 75, 'source': 'Blueprint.counterDamageRate'}
}
}
"""
pass
```
### 4단계: 검증 리포트 생성
각 스킬별 검증 상태를 마크다운으로 출력:
```markdown
## SK100202 - 반격 (Hilda)
**Desc**: 방패를 들어 {0}초 동안 반격 자세를 취합니다. 반격 성공 시 {1}%만큼 물리 피해를 줍니다.
| 변수 | descValues | 실제 값 | 출처 | 상태 |
|------|------------|---------|------|------|
| {0} | 5 | 5 | GA_Skill_Knight_Counter.blockingDuration | ✅ 일치 |
| {1} | 80 | 75 | GA_Skill_Knight_Counter.counterDamageRate | ❌ 불일치 |
**결론**: descValues가 실제 로직과 다를 수 있음 (UI 표시용 값)
```
## 우선순위 스킬 목록
다음 스킬들을 우선적으로 검증:
### High Priority (복잡한 변수 사용)
1. SK100202 (Hilda 반격) - 지속시간, 피해량
2. SK110205 (Urud 다발 화살) - 발사체 개수, 피해량
3. SK160202 (Rene Ifrit 소환) - 지속시간, 공격력
4. SK160206 (Rene Shiva 소환) - 지속시간, 공격 주기
5. SK120202 (Nave 화염벽) - 지속시간, 틱 피해
### Medium Priority (1-2개 변수)
6. SK100201 (Hilda 칼날 격돌) - 피해량, 경직 시간
7. SK130204 (Baran 강제 끌어오기) - 피해량
8. SK180202 (Sinobu 부적폭탄) - 폭발 범위, 피해량
### Low Priority (변수 없거나 단순)
- 대부분의 유틸리티 스킬
- 변수가 없는 스킬
## 예상 결과물
1. **검증 스크립트**: `verify_blueprint_variables.py`
2. **변수 매칭 정의**: `config.py``SKILL_VAR_MAPPING` 추가
3. **검증 리포트**: `Blueprint변수검증_리포트.md`
4. **업데이트된 문서**: 각 스킬에 실제 값 vs descValues 비교 추가
## 예상 소요 시간
- **옵션 A** (Blueprint 재추출): 1-2시간 (추출 설정 + 재실행)
- **옵션 B** (수동 조사): 10-15시간 (91개 스킬 × 평균 10분)
- **옵션 C** (코드 분석): 3-5시간 (C++ 코드 리뷰 + 자동화)
## 권장 접근 방법
1. **단기 (1-2시간)**:
- Blueprint.json 재추출 시도 (FModel 설정 변경)
- 성공 시 자동 검증 스크립트 작성
2. **중기 (3-5시간)**:
- C++ 소스 코드 분석으로 변수 패턴 파악
- 우선순위 High 스킬 5개만 수동 검증
3. **장기 (필요 시)**:
- 모든 스킬 완전 검증
- 자동화 스크립트 완성
- CI/CD 파이프라인에 검증 단계 추가
## 참고 사항
- **descValues는 UI 표시용이므로 실제 로직과 다를 수 있음을 명심**
- 밸런스 패치 시 Blueprint는 업데이트되지만 descValues는 업데이트 안 될 수 있음
- 이 검증은 "문서의 정확성"보다 "게임 로직의 일관성"을 위한 것
- 실제 DPS 계산에는 Blueprint/몽타주의 실제 값을 사용해야 함
## 연락처 및 진행 상황
- **담당자**: (추후 할당)
- **시작일**: 2025-10-24
- **목표 완료일**: (TBD)
- **현재 상태**: 계획 단계
- **진행률**: 0% (정보 수집 대기 중)
---
**생성일**: 2025-10-24 21:34
**버전**: v1.0
**작성자**: Claude Code

View File

@ -0,0 +1,244 @@
# 분석도구 v2 디렉토리 정리 보고서
**정리 일자**: 2025-10-27
**작업자**: AI-assisted Development Team
---
## 📊 정리 요약
### 정리 전
- **총 파일 수**: 26개 (Python 스크립트 24개 + 문서 1개 + 불필요 파일 1개)
- **상태**: 파일이 과도하게 많아 핵심 스크립트 찾기 어려움
### 정리 후
- **메인 디렉토리**: 4개 핵심 스크립트 + 1개 문서
- **utils/**: 2개 유틸리티 스크립트 + README
- **archive/**: 19개 과거 스크립트 + README
- **삭제**: 1개 불필요 파일 (nul)
---
## 📁 최종 디렉토리 구조
```
분석도구/v2/
├── config.py # ⭐ 설정 파일
├── extract_stalker_data_v2.py # ⭐ 데이터 추출
├── generate_stalker_docs_v2.py # ⭐ 문서 생성
├── validate_stalker_data.py # ⭐ 검증
├── 장기과제_Blueprint변수검증.md # 📋 장기 계획 문서
├── utils/ # 🔧 유틸리티 도구
│ ├── list_asset_types.py # Asset 타입 목록
│ ├── list_datatables.py # DataTable 목록
│ └── README.md # 유틸리티 가이드
└── archive/ # 📦 과거 개발 스크립트
├── check_baran_clad_skills.py # 바란/클라드 검증
├── check_bp_vars.py # Blueprint 변수
├── check_bp_verification.py # Blueprint 검증
├── check_character_ability.py # Character Ability 1
├── check_character_ability2.py # Character Ability 2
├── check_character_ability3.py # Character Ability 3
├── check_data.py # 데이터 구조
├── check_first_asset.py # Asset 구조
├── check_improvements.py # 개선사항 검증
├── check_json_structure.py # JSON 구조
├── check_lian_skills.py # 리안 스킬 1
├── check_lian_skills2.py # 리안 스킬 2
├── check_montage_names.py # 몽타주 이름
├── check_send_event_notify.py # SendEvent 노티파이
├── check_sk150201.py # SK150201 분석
├── check_skill_structure.py # 스킬 구조
├── investigate_projectile.py # 투사체 조사
├── verify_improvements.py # 개선사항 검증 1
├── verify_improvements_v2.3.py # 개선사항 검증 2
└── README.md # 아카이브 설명서
```
---
## 🎯 정리 효과
### 1. 가독성 향상
- **정리 전**: 26개 파일이 한 디렉토리에 혼재
- **정리 후**: 5개 핵심 파일만 메인에 노출
- **효과**: 새로운 개발자가 즉시 핵심 스크립트 파악 가능
### 2. 유지보수성 향상
- **정리 전**: 비슷한 이름의 스크립트 다수 (check_character_ability 3개)
- **정리 후**: 역할별로 명확히 분리 (메인/유틸/아카이브)
- **효과**: 수정 필요 시 올바른 파일 즉시 식별
### 3. 프로젝트 구조 명확화
- **메인**: 실제 분석 파이프라인 (추출 → 검증 → 문서화)
- **utils**: 데이터 탐색 도구
- **archive**: 개발 과정 기록
- **효과**: 각 스크립트의 목적과 사용 시점 명확
---
## 🔄 분석 파이프라인 실행 방법
### 기본 실행 (간단)
```bash
# 1단계: 데이터 추출
python extract_stalker_data_v2.py
# 2단계: 검증
python validate_stalker_data.py
# 3단계: 문서 생성
python generate_stalker_docs_v2.py
```
### 유틸리티 사용
```bash
# DataTable 목록 확인
python utils/list_datatables.py
# Asset 타입 확인
python utils/list_asset_types.py
```
### 아카이브 스크립트 참고
```bash
# 특정 스킬 분석이 필요한 경우
# archive/check_sk150201.py를 참고하여 작성
cat archive/check_sk150201.py
```
---
## 📋 파일별 역할 상세
### ⭐ 메인 스크립트 (4개)
#### config.py
- **역할**: 전역 설정 및 상수 정의
- **주요 내용**:
- 데이터 경로 (`DATA_DIR`, `OUTPUT_DIR`)
- 스토커 목록 및 정보 (`STALKERS`, `STALKER_INFO`)
- 공격 스킬 판정 기준 (`ATTACK_NOTIFY_KEYWORDS`)
- DoT/소환 스킬 정의 (`DOT_SKILLS`, `SUMMON_SKILLS`)
- **수정 빈도**: 낮음 (새 스토커 추가 시)
#### extract_stalker_data_v2.py
- **역할**: JSON에서 스토커 데이터 추출
- **입력**: `DataTable.json`, `Blueprint.json`, `AnimMontage.json`
- **출력**: `intermediate_data.json`
- **주요 기능**:
- DT_CharacterStat, DT_CharacterAbility, DT_Skill 추출
- AnimMontage 매칭 및 공격 노티파이 판정
- 소환체 데이터 생성
- **수정 빈도**: 중간 (데이터 구조 변경 시)
#### validate_stalker_data.py
- **역할**: 추출된 데이터 검증
- **입력**: `intermediate_data.json`
- **출력**: `validated_data.json`, `검증_리포트.md`
- **주요 기능**:
- 스탯 합계 검증 (75)
- 스킬 데이터 완전성 확인
- 몽타주 매칭 여부 확인
- **수정 빈도**: 낮음 (검증 규칙 추가 시)
#### generate_stalker_docs_v2.py
- **역할**: 마크다운 문서 생성
- **입력**: `validated_data.json`
- **출력**: `03_스토커별_기본데이터_v2.md`
- **주요 기능**:
- 스토커별 기본 정보 포맷팅
- 스킬 상세 정보 생성
- DoT/소환체 섹션 생성
- **수정 빈도**: 중간 (문서 포맷 변경 시)
### 🔧 유틸리티 스크립트 (2개)
#### utils/list_datatables.py
- **용도**: DataTable.json의 모든 테이블 목록 출력
- **사용 시점**: 새로운 DataTable 추가 여부 확인
#### utils/list_asset_types.py
- **용도**: AnimMontage.json의 Asset 타입 분포 확인
- **사용 시점**: 데이터 구조 변경 탐지
### 📦 아카이브 스크립트 (19개)
**분류별 개수**:
- 스킬 검증: 4개 (baran_clad, lian x2, sk150201)
- 데이터 구조: 4개 (json, first_asset, data, skill)
- Character Ability: 3개 (버전 1, 2, 3)
- AnimMontage/Notify: 3개 (montage_names, send_event, projectile)
- Blueprint: 2개 (bp_vars, bp_verification)
- 개선사항 검증: 3개 (improvements, verify x2)
**재사용 가능성**: 높음 (유사한 문제 발생 시 템플릿으로 활용)
---
## 🗑️ 삭제된 파일
- **nul**: 불필요한 빈 파일
---
## ✅ 검증 결과
### 정리 후 파이프라인 테스트
```bash
# 전체 파이프라인 실행 결과
✅ extract_stalker_data_v2.py: 정상 동작
✅ validate_stalker_data.py: 100% 통과 (109개)
✅ generate_stalker_docs_v2.py: 문서 생성 완료
```
### import 경로 확인
- **config.py**: 절대 경로 사용, 이동 영향 없음 ✅
- **유틸리티 스크립트**: 독립 실행 가능 ✅
- **아카이브 스크립트**: 참고용, 실행 불필요 ✅
---
## 📝 권장 사항
### 1. 디렉토리 구조 유지
- 새로운 스크립트는 역할에 따라 적절한 위치에 추가
- 일회성 스크립트는 즉시 archive/로 이동
### 2. README 업데이트
- 새 유틸리티 추가 시 `utils/README.md` 업데이트
- 중요한 아카이브 스크립트 추가 시 `archive/README.md` 업데이트
### 3. 아카이브 정리
- 6개월~1년 후 archive/ 디렉토리 재검토
- 완전히 불필요한 스크립트 삭제 고려
### 4. 네이밍 규칙
- 메인 스크립트: `{동사}_stalker_{기능}_v2.py`
- 유틸리티: `{동사}_{대상}.py`
- 아카이브: 기존 이름 유지 (히스토리 보존)
---
## 🎉 결론
분석도구 v2 디렉토리 정리가 성공적으로 완료되었습니다.
**개선 효과**:
- ✅ 핵심 스크립트 가시성 80% 향상 (26개 → 5개)
- ✅ 프로젝트 구조 명확화 (3계층 분리)
- ✅ 유지보수성 향상 (README 및 분류 체계)
- ✅ 파이프라인 실행 검증 완료
앞으로 이 구조를 유지하면서 개발을 진행하면 훨씬 효율적인 작업이 가능할 것입니다.
---
**작성자**: AI-assisted Development Team
**정리 완료일**: 2025-10-27

View File

@ -1,74 +0,0 @@
[
{
"AssetName": "AM_PC_Hilda_B_Attack_W01_01",
"AssetPath": "/Game/_Art/_Character/PC/Hilda/AnimMontage/Base/AM_PC_Hilda_B_Attack_W01_01.AM_PC_Hilda_B_Attack_W01_01",
"SequenceLength": 1.6000000238418579,
"RateScale": 1,
"Sections": [
{
"SectionName": "Default",
"StartTime": 0,
"NextSectionName": "None"
}
],
"NumSections": 1,
"SlotAnimTracks": [
{
"SlotName": "DefaultSlot",
"AnimSegments": [
{
"AnimReference": "Ani_PC_Hilda_Base_B_Attack_W01_01",
"AnimPath": "/Game/_Art/_Character/PC/Hilda/Animations/Base/Ani_PC_Hilda_Base_B_Attack_W01_01.Ani_PC_Hilda_Base_B_Attack_W01_01",
"StartPos": 0,
"AnimStartTime": 0,
"AnimEndTime": 1.6000000238418579,
"AnimPlayRate": 1,
"LoopingCount": 1
}
]
}
],
"AnimNotifies": [
{
"NotifyName": "ANS_AttackBlock_C",
"TriggerTime": 0,
"Duration": 0.80000001192092896,
"NotifyType": "NotifyState",
"NotifyStateClass": "ANS_AttackState_C",
"CustomProperties": {
"AddGameplayTags": "(GameplayTags=((TagName=\"Ability.BlockGroup.SubAttack\"),(TagName=\"Character.State.Attack\")))",
"AttackMoveSpeedEffect": "/Script/Engine.BlueprintGeneratedClass'/Game/Blueprints/Abilities/GE_AttackingWalkSpeedDown.GE_AttackingWalkSpeedDown_C'",
"AddNormalAttackPer": "30.000000",
"AddPhysicalAttackPer": "",
"NotifyColor": "(B=200,G=198,R=202,A=255)",
"bShouldFireInEditor": "True"
},
"IsBranchingPoint": true
},
{
"NotifyName": "AttackWithEquip",
"TriggerTime": 0.73333334922790527,
"Duration": 0.13333334028720856,
"NotifyType": "NotifyState",
"NotifyStateClass": "AnimNotifyState_AttackWithEquip",
"CustomProperties": {
"AttackTag": "(TagName=\"Event.Attack.Normal\")",
"PreviewNS": "/Game/_Art/FX/Effects/Common/NS_Hit_DirectionalE001.NS_Hit_DirectionalE001",
"bUseEffectNormal": "True",
"EffectNormal": "(X=0.000000,Y=-1.500000,Z=-1.000000)",
"SocketName": "socket_R_Weapon",
"bSendShotEventToActor": "True",
"NotifyColor": "(B=111,G=0,R=255,A=255)",
"bShouldFireInEditor": "True"
},
"IsBranchingPoint": true
}
],
"BlendInTime": 0.25,
"BlendOutTime": 0.25,
"BlendOutTriggerTime": -1,
"BlendModeIn": "Standard",
"BlendModeOut": "Standard",
"Notes": "샘플: Hilda 기본 공격 1타 몽타주 (주요 Notify만 포함, 구조 참고용)"
}
]

View File

@ -1,77 +0,0 @@
[
{
"AssetName": "GA_Skill_Hilda_SwordStrike",
"ParentClass": "GA_Skill_Knight_LeapAttack_C",
"Variables": [
{
"Name": "bActiveOnGive",
"Type": "bool",
"DefaultValue": "False",
"IsEditable": true,
"IsBlueprintVisible": false,
"IsBlueprintReadOnly": false,
"IsEditDefaultsOnly": true,
"CategoryName": "WorldStalker",
"Source": "C++ParentClass",
"OwnerClass": "WSGameplayAbility"
},
{
"Name": "ActivationOrderGroup",
"Type": "uint8",
"DefaultValue": "4",
"IsEditable": true,
"IsBlueprintVisible": false,
"IsBlueprintReadOnly": false,
"IsEditDefaultsOnly": true,
"CategoryName": "WorldStalker",
"Source": "C++ParentClass",
"OwnerClass": "WSGameplayAbility"
},
{
"Name": "AttackEffectClass",
"Type": "TSoftClassPtr<UGameplayEffect> ",
"DefaultValue": "/Game/Blueprints/Abilities/GE_Attack_Ability.GE_Attack_Ability_C",
"IsEditable": true,
"IsBlueprintVisible": false,
"IsBlueprintReadOnly": false,
"IsEditDefaultsOnly": true,
"CategoryName": "WorldStalker",
"Source": "C++ParentClass",
"OwnerClass": "WSGameplayAbility"
},
{
"Name": "ManaCostEffectClass",
"Type": "TSoftClassPtr<UGameplayEffect> ",
"DefaultValue": "/Game/Blueprints/Abilities/GE_Skill_ManaCost.GE_Skill_ManaCost_C",
"IsEditable": true,
"IsBlueprintVisible": false,
"IsBlueprintReadOnly": false,
"IsEditDefaultsOnly": true,
"CategoryName": "WorldStalker",
"Source": "C++ParentClass",
"OwnerClass": "WSGameplayAbility"
},
{
"Name": "CoolTimeEffectClass",
"Type": "TSoftClassPtr<UGameplayEffect> ",
"DefaultValue": "/Game/Blueprints/Abilities/GE_Skill_CoolTime.GE_Skill_CoolTime_C",
"IsEditable": true,
"IsBlueprintVisible": false,
"IsBlueprintReadOnly": false,
"IsEditDefaultsOnly": true,
"CategoryName": "WorldStalker",
"Source": "C++ParentClass",
"OwnerClass": "WSGameplayAbility"
}
],
"EventGraphs": [
{
"GraphName": "EventGraph",
"Nodes": [
"(예시) 실제로는 수십~수백 개의 노드가 포함됨"
]
}
],
"Notes": "샘플: Hilda SwordStrike 스킬 Blueprint (주요 Variables만 포함, 구조 참고용)"
}
]

View File

@ -1,67 +0,0 @@
[
{
"AssetName": "DT_CharacterStat",
"AssetPath": "/Game/Blueprints/DataTable/DT_CharacterStat.DT_CharacterStat",
"RowStructure": "CharacterStatData",
"Rows": [
{
"RowName": "hilda",
"Data": {
"name": "힐다",
"jobName": "전사",
"capsuleRadius": 34,
"str": 20,
"dex": 15,
"int": 10,
"con": 20,
"wis": 10,
"hP": 100,
"mP": 50,
"manaRegen": 0.20000000298023224,
"stamina": 100,
"physicalDamage": 0,
"magicalDamage": 0,
"criticalPer": 5,
"criticalDamage": 0,
"backAttackDamage": 0,
"defense": 0,
"physicalResistancePer": 0,
"rangedResistancePer": 0,
"magicalResistancePer": 0,
"fireResistancePer": 0,
"poisonResistancePer": 0,
"waterResistancePer": 0,
"lightningResistancePer": 0,
"holyResistancePer": 0,
"darkResistancePer": 0,
"dOTReduceRatePer": 0,
"walkSpeed": 0,
"defaultSkills": [
"SK100201",
"SK100202",
"SK100204"
],
"subSkill": "SK100101",
"ultimateSkill": "SK100301",
"abilities": [],
"tags": {
"gameplayTags": []
},
"montageMap": {},
"defaultEquip": {},
"equipableTypes": [
"WeaponShield",
"Heavy",
"Light"
],
"hitRadius": 170,
"ultimatePoint": 2495,
"breakdownMax": -1,
"breakdownStunTime": 0,
"breakdownResetTime": 0
}
}
],
"Notes": "샘플: Hilda 스토커 기본 스탯 데이터 (구조 참고용)"
}
]

View File

@ -1,72 +0,0 @@
{
"GE_Skill_Nave_Escape": {
"name": "GE_Skill_Nave_Escape",
"variables": [],
"event_graphs": [
""
]
},
"GE_Skill_Nave_Escape_Active": {
"name": "GE_Skill_Nave_Escape_Active",
"variables": [],
"event_graphs": [
""
]
},
"GE_Attack_Projectile_Splash": {
"name": "GE_Attack_Projectile_Splash",
"variables": [],
"event_graphs": [
""
]
},
"GE_StunMotion": {
"name": "GE_StunMotion",
"variables": [],
"event_graphs": [
""
]
},
"GE_Skill_Rio_Sensitive_Active": {
"name": "GE_Skill_Rio_Sensitive_Active",
"variables": [],
"event_graphs": [
""
]
},
"GE_Skill_Lian_ManaStoneSilence": {
"name": "GE_Skill_Lian_ManaStoneSilence",
"variables": [],
"event_graphs": [
""
]
},
"GE_Attack_Splash_Physical": {
"name": "GE_Attack_Splash_Physical",
"variables": [],
"event_graphs": [
""
]
},
"GE_Skill_Sinobu_Silence": {
"name": "GE_Skill_Sinobu_Silence",
"variables": [],
"event_graphs": [
""
]
},
"GE_Skill_Hilda_BloodMoon_Active": {
"name": "GE_Skill_Hilda_BloodMoon_Active",
"variables": [],
"event_graphs": [
""
]
},
"GE_Skill_Urud_Explosion": {
"name": "GE_Skill_Urud_Explosion",
"variables": [],
"event_graphs": [
""
]
}
}

View File

@ -1,232 +0,0 @@
<EFBFBD>м<EFBFBD> <20><>: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\20251023_223849\DataTable.json
=== <20><><EFBFBD><EFBFBD>Ŀ <20><20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ===
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: 10<31><30>
=== <20><><EFBFBD><EFBFBD>Ŀ <20><>Ÿ <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD> ===
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: 10<31><30>
=== <20><><EFBFBD><EFBFBD>Ŀ <20><>ų <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ===
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: 10<31><30>
================================================================================
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>١<EFBFBD> (<28><><EFBFBD><EFBFBD>)
================================================================================
STR: 20 | DEX: 15 | INT: 10 | CON: 20 | WIS: 10
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: WeaponShield, Heavy, Light
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2495
[<5B><20><>ų]
- Į<><C4AE> <20>ݵ<EFBFBD> (SK100201): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 6<><36> | <20><><EFBFBD><EFBFBD>: 11
- <20>ݰ<EFBFBD> (SK100202): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 4<><34> | <20><><EFBFBD><EFBFBD>: 10
- <20><><EFBFBD><EFBFBD> (SK100204): Normal | <20><>Ÿ<EFBFBD><C5B8>: 10<31><30> | <20><><EFBFBD><EFBFBD>: 8
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> (SK100101): Normal | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD>ͺ<EFBFBD> <20>ޡ<EFBFBD> (SK100301): Normal
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- weaponShield: 3Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>塽 (<28><><EFBFBD>Ÿ<EFBFBD>)
================================================================================
STR: 15 | DEX: 20 | INT: 10 | CON: 15 | WIS: 15
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: Bow, Light, Cloth
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2623
[<5B><20><>ų]
- <20>ٹ<EFBFBD> ȭ<><C8AD> (SK110205): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7<><37> | <20><><EFBFBD><EFBFBD>: 14
- <20><><EFBFBD><EFBFBD> ȭ<><C8AD> (SK110204): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7<><37> | <20><><EFBFBD><EFBFBD>: 9
- <20><> <20><>ġ (SK110201): Normal | <20><>Ÿ<EFBFBD><C5B8>: 5<><35> | <20><><EFBFBD><EFBFBD>: 9
- Reload (SK110207): Normal | <20><>Ÿ<EFBFBD><C5B8>: 0<><30> | <20><><EFBFBD><EFBFBD>: 0
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- ȭ<><C8AD> <20><EFBFBD><EEB8A3> (SK110101): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>⡯ (SK110301): Normal
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- bow: 1Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̺꡽ (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
================================================================================
STR: 10 | DEX: 10 | INT: 25 | CON: 10 | WIS: 20
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: Staff, Light, Cloth
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2728
[<5B><20><>ų]
- <20><><EFBFBD><EFBFBD> ȭ<><C8AD> (SK120201): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 3.5<EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD>: 18
- ȭ<><C8AD><EFBFBD><EFBFBD> (SK120202): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 5<><35> | <20><><EFBFBD><EFBFBD>: 25
- <20><><EFBFBD><EFBFBD><EFBFBD>ٶ<EFBFBD> (SK120206): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7<><37> | <20><><EFBFBD><EFBFBD>: 9
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> (SK120101): Normal | <20><>Ÿ<EFBFBD><C5B8>: 1<><31>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD>ع桯 (SK120301): MagicalSkill
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- staff: 2Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD><EFBFBD>ٶ<EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>)
================================================================================
STR: 25 | DEX: 10 | INT: 5 | CON: 25 | WIS: 10
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: TwoHandWeapon, Heavy, Light
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2780
[<5B><20><>ų]
- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><>ô (SK130204): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 13<31><33> | <20><><EFBFBD><EFBFBD>: 14
- <20>ķ<EFBFBD>ġ<EFBFBD><C4A1> (SK130203): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 8<><38> | <20><><EFBFBD><EFBFBD>: 9
- <20><><EFBFBD><EFBFBD> <20><EFBFBD><EEB8A3> (SK130206): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7<><37> | <20><><EFBFBD><EFBFBD>: 10
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> (SK130101): Normal | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> '<27>ϰݺм<DDBA>' (SK130301): PhysicalSkill
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- twoHandWeapon: 3Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28>ϻ<EFBFBD><CFBB><EFBFBD>)
================================================================================
STR: 15 | DEX: 25 | INT: 10 | CON: 15 | WIS: 10
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: ShortSword, Cloth, Light
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2368
[<5B><20><>ų]
- <20><><EFBFBD><EFBFBD> <20><EFBFBD><EEB8A3> (SK140201): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 3.5<EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD>: 9
- <20><><EFBFBD><EFBFBD> (SK140205): Normal | <20><>Ÿ<EFBFBD><C5B8>: 4<><34> | <20><><EFBFBD><EFBFBD>: 8
- <20>ܰ<EFBFBD> <20><>ô (SK140202): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7<><37> | <20><><EFBFBD><EFBFBD>: 10
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> (SK140101): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD>ΰ<EFBFBD><CEB0><EFBFBD> (SK140301): Normal
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- shortSword: 3Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD>Ŭ<EFBFBD><EFBFBD><EFBFBD>塽 (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
================================================================================
STR: 15 | DEX: 10 | INT: 10 | CON: 20 | WIS: 20
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: Mace, Heavy, Light
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2325
[<5B><20><>ų]
- ġ<><C4A1> (SK150206): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 3<><33> | <20><><EFBFBD><EFBFBD>: 12
- <20>ٽ<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (SK150201): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 5<><35> | <20><><EFBFBD><EFBFBD>: 9
- <20>ż<EFBFBD><C5BC><EFBFBD> <20><> (SK150202): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7.5<EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD>: 15
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> (SK150101): Normal | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> <20><>Ȳ<EFBFBD>ݡ<EFBFBD> (SK150301): Normal
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- mace: 2Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ס<EFBFBD> (<28><>ȯ<EFBFBD><C8AF>)
================================================================================
STR: 10 | DEX: 10 | INT: 20 | CON: 10 | WIS: 25
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: Staff, Light, Cloth
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2305
[<5B><20><>ų]
- <20><><EFBFBD><EFBFBD> <20><>ȯ : ȭ<><C8AD> (SK160202): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7<><37> | <20><><EFBFBD><EFBFBD>: 8
- <20><><EFBFBD><EFBFBD> <20><>ȯ : <20>ñ<EFBFBD> (SK160206): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 10<31><30> | <20><><EFBFBD><EFBFBD>: 15
- <20><><EFBFBD><EFBFBD> ȭ<><C8AD> (SK160203): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 10<31><30> | <20><><EFBFBD><EFBFBD>: 15
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (SK160101): MagicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (SK160301): MagicalSkill
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- staff: 3Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD><EFBFBD>ó<EFBFBD><EFBFBD>Ρ<EFBFBD> (<28><><EFBFBD><EFBFBD>)
================================================================================
STR: 10 | DEX: 25 | INT: 10 | CON: 15 | WIS: 15
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: ShortSword, Cloth, Light
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2035
[<5B><20><>ų]
- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (SK180202): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 6<><36> | <20><><EFBFBD><EFBFBD>: 10
- <20><><EFBFBD>ڰ<EFBFBD> (SK180203): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 8<><38> | <20><><EFBFBD><EFBFBD>: 11
- <20>μ<EFBFBD> <20><><EFBFBD>ٲ<EFBFBD>ġ<EFBFBD>⡯ (SK180205): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 11<31><31> | <20><><EFBFBD><EFBFBD>: 12
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- ǥâ (SK180101): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> '<27><>ȯ' (SK180301): Normal
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- shortSword: 2Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
================================================================================
STR: 10 | DEX: 20 | INT: 10 | CON: 15 | WIS: 20
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: Bow, Light, Cloth
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2775
[<5B><20><>ų]
- <20>ӻ<EFBFBD> (SK190207): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7<><37> | <20><><EFBFBD><EFBFBD>: 16
- <20>񿬻<EFBFBD> (SK190205): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 10<31><30> | <20><><EFBFBD><EFBFBD>: 15
- <20><>ȭ (SK190201): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 7.5<EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD>: 12
- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (SK190209): Normal | <20><>Ÿ<EFBFBD><C5B8>: 0<><30> | <20><><EFBFBD><EFBFBD>: 0
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (SK190101): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD> '<27><><EFBFBD><EFBFBD>' (SK190301): PhysicalSkill
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- bow: 1Ÿ <20><><EFBFBD><EFBFBD>
================================================================================
<EFBFBD><EFBFBD>ī<EFBFBD><EFBFBD><EFBFBD>𸣵塽 (<28><><EFBFBD><EFBFBD>)
================================================================================
STR: 15 | DEX: 25 | INT: 10 | CON: 15 | WIS: 10
HP: 100 | MP: 50 | Mana Regen: 0.20000000298023224
<EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>: WeaponShield, Light, Cloth
<EFBFBD>ñر<EFBFBD> <20><><EFBFBD><EFBFBD>Ʈ: 2368
[<5B><20><>ų]
- <20><><EFBFBD><EFBFBD> (SK170201): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 15.5<EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD>: 5
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> (SK170202): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 15.5<EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD>: 10
- <20>ۿ<EFBFBD> (SK170203): Normal | <20><>Ÿ<EFBFBD><C5B8>: 27.5<EFBFBD><EFBFBD> | <20><><EFBFBD><EFBFBD>: 3
[<5B><><EFBFBD><EFBFBD> <20><>ų]
- <20><EFBFBD><EAB8AE> (SK170101): PhysicalSkill | <20><>Ÿ<EFBFBD><C5B8>: 0<><30>
[<5B>ñر<C3B1>]
- <20><><EFBFBD><EFBFBD><><C4AE><EFBFBD><EFBFBD>dz' (SK170301): PhysicalSkill
[<5B><>Ÿ <20><>Ÿ<EFBFBD><C5B8>]
- weaponShield: 3Ÿ <20><><EFBFBD><EFBFBD>

View File

@ -1,133 +0,0 @@
{
"stalker_ges": {
"hilda": [
{
"name": "GE_Skill_Hilda_BloodMoon_Active_GE_Skill_Hilda_BloodMoon_Active",
"class": "/Game/Blueprints/Abilities/GE_Skill_Hilda_BloodMoon_Active.GE_Skill_Hilda_BloodMoon_Active_C",
"trigger": "InActive",
"tagValues": [
{
"tag": {
"tagName": "Data.Value"
},
"value": 15
},
{
"tag": {
"tagName": "Data.Value2"
},
"value": 25
}
]
}
],
"urud": [
{
"name": "GE_Skill_Urud_Explosion_GE_Skill_Urud_Explosion",
"class": "/Game/Blueprints/Characters/Urud/GE_Skill_Urud_Explosion.GE_Skill_Urud_Explosion_C",
"trigger": "InActive",
"tagValues": []
},
{
"name": "GE_Attack_Projectile_Splash_GE_Attack_Projectile_Splash",
"class": "/Game/Blueprints/Abilities/GE_Attack_Projectile_Splash.GE_Attack_Projectile_Splash_C",
"trigger": "OnProjectileHitRangedTarget",
"tagValues": [
{
"tag": {
"tagName": "Data.Value"
},
"value": 0.30000001192092896
},
{
"tag": {
"tagName": "Data.SkillRate"
},
"value": 0.30000001192092896
}
]
}
],
"nave": [
{
"name": "GE_Skill_Nave_Escape_Active_GE_Skill_Nave_Escape_Active",
"class": "/Game/Blueprints/Characters/Nave/GE_Skill_Nave_Escape_Active.GE_Skill_Nave_Escape_Active_C",
"trigger": "InActive",
"tagValues": []
},
{
"name": "GE_Skill_Nave_Escape_GE_Skill_Nave_Escape",
"class": "/Game/Blueprints/Characters/Nave/GE_Skill_Nave_Escape.GE_Skill_Nave_Escape_C",
"trigger": "OnActiveRangedTarget",
"tagValues": [
{
"tag": {
"tagName": "Skill.Effect.SkillPer"
},
"value": 0
}
]
}
],
"baran": [
{
"name": "GE_StunMotion_GE_StunMotion",
"class": "/Game/Blueprints/Abilities/GE_StunMotion.GE_StunMotion_C",
"trigger": "CustomEventTarget1",
"tagValues": [
{
"tag": {
"tagName": "Data.Duration"
},
"value": 3
}
]
},
{
"name": "GE_Attack_Splash_Physical_GE_Attack_Splash_Physical",
"class": "/Game/Blueprints/Abilities/GE_Attack_Splash_Physical.GE_Attack_Splash_Physical_C",
"trigger": "CustomEventTarget1",
"tagValues": []
}
],
"rio": [
{
"name": "GE_Skill_Rio_Sensitive_Active_GE_Skill_Rio_Sensitive_Active",
"class": "/Game/Blueprints/Characters/Rio/GE_Skill_Rio_Sensitive_Active.GE_Skill_Rio_Sensitive_Active_C",
"trigger": "InActive",
"tagValues": []
}
],
"clad": [],
"rene": [],
"sinobu": [
{
"name": "GE_Skill_Sinobu_Silence_GE_Skill_Sinobu_Silence",
"class": "/Game/Blueprints/Characters/Sinobu/GE_Skill_Sinobu_Silence.GE_Skill_Sinobu_Silence_C",
"trigger": "InActive",
"tagValues": []
}
],
"lian": [
{
"name": "GE_Skill_Lian_ManaStoneSilence_GE_Skill_Lian_ManaStoneSilence",
"class": "/Game/Blueprints/Characters/Lian/GE_Skill_Lian_ManaStoneSilence.GE_Skill_Lian_ManaStoneSilence_C",
"trigger": "InActive",
"tagValues": []
}
],
"cazimord": []
},
"all_ge_classes": [
"/Game/Blueprints/Characters/Nave/GE_Skill_Nave_Escape.GE_Skill_Nave_Escape_C",
"/Game/Blueprints/Characters/Nave/GE_Skill_Nave_Escape_Active.GE_Skill_Nave_Escape_Active_C",
"/Game/Blueprints/Abilities/GE_Attack_Projectile_Splash.GE_Attack_Projectile_Splash_C",
"/Game/Blueprints/Abilities/GE_StunMotion.GE_StunMotion_C",
"/Game/Blueprints/Characters/Rio/GE_Skill_Rio_Sensitive_Active.GE_Skill_Rio_Sensitive_Active_C",
"/Game/Blueprints/Characters/Lian/GE_Skill_Lian_ManaStoneSilence.GE_Skill_Lian_ManaStoneSilence_C",
"/Game/Blueprints/Abilities/GE_Attack_Splash_Physical.GE_Attack_Splash_Physical_C",
"/Game/Blueprints/Characters/Sinobu/GE_Skill_Sinobu_Silence.GE_Skill_Sinobu_Silence_C",
"/Game/Blueprints/Abilities/GE_Skill_Hilda_BloodMoon_Active.GE_Skill_Hilda_BloodMoon_Active_C",
"/Game/Blueprints/Characters/Urud/GE_Skill_Urud_Explosion.GE_Skill_Urud_Explosion_C"
]
}

File diff suppressed because one or more lines are too long