v2 폐기하고 v3로 새출발

This commit is contained in:
Gnill82
2025-11-05 11:09:16 +09:00
parent e17020143f
commit 80176c7c9e
64 changed files with 378 additions and 263 deletions

1468
legacy/ARCHITECTURE.md Normal file

File diff suppressed because it is too large Load Diff

372
legacy/README.md Normal file
View File

@ -0,0 +1,372 @@
# DS-전투분석 저장소
던전 스토커즈(DungeonStalkers) 전투 시스템 종합 분석 저장소입니다.
## 프로젝트 개요
### 목적
본 프로젝트는 언리얼 엔진 5.5.4 기반의 던전 스토커즈 전투 시스템을 체계적으로 분석하고 문서화하여 다음 목표를 달성합니다:
- **밸런스 분석**: 10명 스토커의 전투 능력 비교 및 밸런스 검증
- **시스템 문서화**: Gameplay Ability System 기반 전투 로직 상세 분석
- **정기 모니터링**: 패치/업데이트 시 변경사항 추적 및 영향도 분석
- **팀 공유**: 기획자, 프로그래머, QA가 공통으로 참고할 수 있는 기술 문서
### 분석 대상
- **10명의 스토커**: Hilda, Urud, Nave, Baran, Rio, Clad, Rene, Sinobu, Lian, Cazimord
- **전투 시스템 요소**:
- 기본 공격 타이밍 및 피해 배율
- 스킬 시스템 (Gameplay Ability System)
- 캔슬 메커니즘 (Activation Order Group + ANS_SkillCancel_C)
- 애니메이션 노티파이 시스템
- 캐릭터 스탯 및 속성
## 분석 방법론
### 1. 데이터 추출 (Unreal → JSON)
언리얼 에디터의 커스텀 익스포터를 사용하여 게임 에셋을 JSON 형식으로 변환합니다.
```
Unreal Engine Assets → Custom Exporter → JSON Files
```
**익스포트 대상**:
- `DataTable` → DT_CharacterStat, DT_Skill, DT_CharacterAbility 등
- `AnimMontage` → 모든 캐릭터 애니메이션 몽타주
- `Blueprint` → GA_* (Gameplay Ability) 블루프린트
- `CurveTable` → 각종 커브 데이터
**익스포트 방법**:
1. 언리얼 에디터에서 Content Browser 열기
2. 분석할 에셋 선택
3. 우클릭 → `Export to JSON` (커스텀 익스포터)
4. 출력 폴더에 JSON 파일 생성
### 2. LLM 기반 분석
생성된 JSON 파일을 Claude (LLM)에 입력하여 자동 분석합니다.
**분석 프로세스**:
```
JSON Files → Claude Code → Analysis Document
Python Scripts (보조 분석)
```
**LLM의 역할**:
- 대용량 JSON 데이터 파싱 및 패턴 인식
- 스토커별 데이터 비교 분석
- 전투 로직 추론 및 시스템 메커니즘 분석
- Markdown 형식의 기술 문서 자동 생성
**장점**:
- 수작업 대비 100배 이상 빠른 분석 속도
- 일관된 형식의 문서 생성
- 복잡한 크로스 레퍼런스 추적 자동화
### 3. 검증 및 문서화
분석 결과를 검토하고 최종 문서를 생성합니다.
---
## v2 분석 프로세스 (2025-10 업데이트)
### 프로세스 개요
v2 시스템은 JSON 원본에서 밸런스 리포트까지 **4단계 자동화 파이프라인**입니다.
```
[원본 JSON] → 01단계 → 02단계 → 03단계 → 04단계
기본데이터 DPS계산 역할비교 밸런스티어
```
### 🔥 v2.1 주요 업데이트 (2025-10-28)
**Game Changer 발견!**
#### 1. 콤보 캔슬 시스템 발견
- `ANS_DisableBlockingState_C` 노티파이로 평타 조기 캔슬 가능
- **클라드**: 평타 시간 56% 단축 → **평타 DPS 1위** (125.5)
- **힐다**: 19% 단축, 탱커 최고 DPS (107.3)
- **바란**: 19% 단축, 중상위권 (90.4)
#### 2. 바란 궁극기 시전시간 정정
- DT_Skill 10초 → **실제 1.29초** (AN_SimpleSendEvent 시점)
- 10초는 최대 홀딩 시간 (타이밍 조절 가능)
- 스크립트 자동 감지 및 오버라이드
#### 3. 15초 버스트 시나리오 확대
- 기존 10초 → **15초**로 확대 (궁극기 지속시간 반영)
- 레네 궁극기 실전 필수 (흡혈 생존력)
- 종합 티어표 3개 시나리오 통합 평가
### 4단계 구성
#### 01단계: 스토커별 기본 데이터
- **목적**: JSON에서 10명 스토커 정보 추출 및 검증
- **출력**: `01_스토커별_기본데이터_v2.md`
- **도구**: `분석도구/v2/` (extract → validate → generate)
#### 02단계: DPS 시나리오 비교분석
- **목적**: 3개 시나리오 DPS 계산 (평타, 로테이션, 버스트)
- **출력**: `02_DPS_시나리오_비교분석_v2.md`
- **도구**: `calculate_dps_scenarios_v2.py`
#### 03단계: 역할별 차별화
- **목적**: 5개 역할군 비교 (전사, 원거리, 마법사, 암살자, 서포터)
- **출력**: `03_역할별_차별화_v2.md`
#### 04단계: 밸런스 티어 및 개선안
- **목적**: OP/S+/S/A/B 티어 평가 및 밸런스 개선안
- **출력**: `04_밸런스_티어_및_개선안_v2.md`
### 실행 방법
**전체 프로세스**:
```bash
# 1단계: 기본 데이터 추출 및 검증
cd 분석도구/v2
python extract_stalker_data_v2.py
python validate_stalker_data.py
python generate_stalker_docs_v2.py
# 2단계: DPS 계산
python calculate_dps_scenarios_v2.py
# 3~4단계: 추후 구현 예정
```
**출력 구조**:
```
분석결과/YYYYMMDD_HHMMSS_v2/
├── 01_스토커별_기본데이터_v2.md
├── 02_DPS_시나리오_비교분석_v2.md
├── 03_역할별_차별화_v2.md
├── 04_밸런스_티어_및_개선안_v2.md
├── intermediate_data.json
├── validated_data.json
└── 검증_리포트.md
```
### 상세 문서
- [ARCHITECTURE.md](ARCHITECTURE.md) - v2 프로세스 완전 상세 가이드 (알고리즘, 체크리스트 포함)
---
## 폴더 구조
```
DS-전투분석_저장소/
├── README.md # 본 문서
├── ARCHITECTURE.md # 기술 아키텍처 (v2 프로세스 상세)
├── 분석결과/ # 분석 결과물
│ ├── 20251024_000515/ # 기존 분석 (참고용)
│ └── YYYYMMDD_HHMMSS_v2/ # v2 분석 결과
│ ├── 01_스토커별_기본데이터_v2.md
│ ├── 02_DPS_시나리오_비교분석_v2.md
│ ├── 03_역할별_차별화_v2.md
│ ├── 04_밸런스_티어_및_개선안_v2.md
│ ├── intermediate_data.json
│ ├── validated_data.json
│ └── 검증_리포트.md
├── 원본데이터/ # JSON 원본 데이터
│ ├── DataTable.json
│ ├── AnimMontage.json
│ ├── Blueprint.json
│ └── CurveTable.json
└── 분석도구/ # Python 분석 스크립트
├── v2/ # v2 자동화 도구
│ ├── extract_stalker_data_v2.py
│ ├── validate_stalker_data.py
│ ├── generate_stalker_docs_v2.py
│ ├── calculate_dps_scenarios_v2.py # 개발 예정
│ └── config.py
└── utils/ # 유틸리티 스크립트
└── ...
```
## 정기 분석 수행 가이드
새로운 분석을 수행하려면 다음 단계를 따르세요.
### Step 1: JSON 데이터 익스포트
**언리얼 에디터에서 수행**:
```
1. Content Browser에서 다음 폴더들을 선택:
- /Game/Blueprints/DataTable/
- /Game/_Art/_Character/PC/*/AnimMontage/
- /Game/Blueprints/Abilities/GA_Skill_*/
2. 우클릭 → Export to JSON
3. 출력 폴더 선택: DS-전투밸런스_분석자료/[오늘날짜]/
예: DS-전투밸런스_분석자료/20251024_153000/
4. 익스포트 완료 확인:
✓ DataTable.json
✓ AnimMontage.json
✓ Blueprint.json
✓ CurveTable.json
```
### Step 2: LLM 분석 실행
**Claude Code 사용**:
1. Claude Code CLI 실행
2. 다음 프롬프트 입력:
```
"DS-전투밸런스_분석자료/[날짜]/" 폴더의 JSON 파일들을 분석하여
전투 시스템 종합 분석 문서를 작성해주세요.
분석 항목:
- 10명 스토커별 기본 공격 타이밍 및 피해 배율
- 스킬별 Activation Order Group 값
- 애니메이션 캔슬 윈도우 (ANS_SkillCancel_C)
- 캐릭터 스탯 비교
이전 분석 문서를 참고하여 동일한 형식으로 작성하고,
변경사항이 있다면 별도로 표시해주세요.
```
3. 생성된 문서를 검토하고 수정
### Step 3: 결과 저장
```bash
# 새 폴더 생성
mkdir -p 분석결과/[날짜]
# 분석 문서 저장
# Claude가 생성한 문서를 분석결과/[날짜]/DS-전투시스템_종합분석.md로 저장
# (선택) 원본 JSON 샘플 저장
# 주요 에셋 몇 개만 추출하여 원본데이터/[날짜]/에 저장
```
### Step 4: 변경사항 추적
**이전 분석과 비교**:
```bash
# diff 도구로 변경 확인
diff 분석결과/20251023/DS-전투시스템_종합분석.md \
분석결과/20251024/DS-전투시스템_종합분석.md
```
**주요 확인 사항**:
- ActivationOrderGroup 변경 (밸런스 조정)
- 기본 공격 타이밍 변경 (애니메이션 수정)
- AddNormalAttackPer 변경 (피해 배율 조정)
- 새로운 스킬 추가/삭제
## 분석 도구 사용법
### 1. 스킬 캔슬 윈도우 추출
```bash
python 분석도구/extract_skill_cancel_windows.py \
원본데이터/20251023/AnimMontage.json
```
**출력 예시**:
```
AM_PC_Hilda_B_Skill_SwordStrike
캔슬 구간: 1.300s ~ 1.800s (지속: 0.500s)
```
### 2. 캐릭터 스탯 분석
```bash
python 분석도구/analyze_character_stats.py \
원본데이터/20251023/DataTable.json
```
**출력 예시**:
```
이름 직업 STR DEX INT CON WIS
힐다 전사 20 15 10 20 10
우르드 원거리 15 20 10 15 15
```
### 3. Activation Order Group 추출
```bash
python 분석도구/extract_activation_order_groups.py \
원본데이터/20251023/Blueprint.json
```
**출력 예시**:
```
Hilda:
Group 4: Bash, SwordStrike
Group 0: BloodMoon_Active, SteelBlocking
```
## 최신 분석 결과
**날짜**: 2025-10-27 (v2.1)
**분석 문서**: `분석결과/20251027_200151_v2/`
**주요 발견 (v2.1)**:
- **평타 DPS 1위**: 클라드 (125.5) - 콤보 캔슬로 137% 증가!
- **30초 로테이션 1위**: 레네 (158.88) - 소환수 독립 DPS
- **15초 버스트 1위**: 카지모르드 (165.1) - Parrying + 궁극기
- **종합 S티어**: 시노부, 레네 (모든 시나리오 안정적 상위권)
- **밸런스 개선 필요**: 리안, 우르드 (재장전/충전 페널티 과다)
## 기술 스택
- **게임 엔진**: Unreal Engine 5.5.4
- **에셋 익스포터**: Custom Unreal Editor Plugin
- **분석 LLM**: Claude 3.5 Sonnet (Claude Code)
- **보조 분석**: Python 3.x
- **문서 형식**: Markdown
## 참고 자료
### 내부 문서
- [DS-전투시스템_종합분석.md](분석결과/20251023/DS-전투시스템_종합분석.md) - 최신 분석 결과
- [CLAUDE.md](../CLAUDE.md) - 프로젝트 전체 개요
### 외부 참고
- [Unreal Engine 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/)
## 팀원 기여
분석 결과에 피드백이나 추가 분석 요청이 있으시면:
1. 이슈 등록 (Git Issue)
2. 또는 디스코드 공식 커뮤니티에 공유
---
**마지막 업데이트**: 2025-10-28 (v2.1)
**담당자**: AI-assisted Analysis Team
## 변경 이력
### v2.1 (2025-10-28)
- **콤보 캔슬 시스템 발견** (ANS_DisableBlockingState_C)
- 바란 궁극기 시전시간 정정 (10초 → 1.29초)
- 15초 버스트 시나리오로 확대 (10초 → 15초)
- 종합 티어표 3개 시나리오 통합 평가
- 밸런스 개선 제안 (리안, 우르드, 네이브)
### v2.0 (2025-10-27)
- 4단계 자동화 파이프라인 구축
- 01~02단계 문서 생성 완료
- 소환수 독립 DPS 계산 (레네)
- DoT 시스템 분석 (Poison, Burn, Bleed)

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,253 @@
# 01. 던전 스토커즈 전투 밸런스 분석 - 요약
## 분석 개요
**분석 일시**: 2025-10-24
**분석 대상**: 10명 스토커 (Hilda, Urud, Nave, Baran, Rio, Clad, Rene, Sinobu, Lian, Cazimord)
**분석 기준**: 레벨 20, 기어스코어 400, 최적 플레이 (100% 활용)
**데이터 소스**: DT_CharacterStat, DT_CharacterAbility, DT_Skill, GameplayEffect Blueprints
---
## 핵심 발견사항
### 1. DPS 분석 결과
#### 지속 DPS 순위 (30초 스킬 로테이션)
| 순위 | 스토커 | 지속 DPS | 역할 | 상태 |
|------|--------|----------|------|------|
| 1 | **Rio** | **268** | 암살자 | ⚠️ 과다 (+21% vs 2위) |
| 2 | **Cazimord** | 221 | 전사 | ✅ 양호 (패링 100%) |
| 3 | **Lian** | 219 | 원거리 | ✅ 양호 |
| 4 | **Nave** | 202 | 마법사 | ✅ 양호 |
| 5 | **Sinobu** | 176 | 암살자 | ✅ 양호 |
| 6 | **Rene** | 148 | 마법사 | ✅ 양호 |
| 7 | **Baran** | 128 | 전사 | 🔶 검토 |
| 8 | **Hilda** | 117 | 전사 | ✅ 양호 |
| 9 | **Urud** | 82 | 원거리 | ⚠️ 부족 |
| 10 | **Clad** | 76 | 서포터 | ✅ 양호 |
**주요 이슈**:
- **Rio**: 2위 Cazimord(221)보다 +47 (+21%) 과다 → **너프 필요**
- **Urud**: 서포터 Clad(76)와 비슷한 82 → **버프 필요**
- **DPS 격차**: 최대 3.3배 (Rio 268 vs Urud 82, 서포터 제외)
---
#### 버스트 DPS 순위 (10초 풀콤보, 궁극기 포함)
| 순위 | 스토커 | 버스트 DPS | 궁극기 | 특징 |
|------|--------|------------|--------|------|
| 1 | **Cazimord** | **256** | 칼날폭풍 (10.0배) | 단일 대상 최강 (12연타) |
| 2 | **Nave** | **241** | 해방 (10.0배) | 관통 광역 (다수 적 시 최강) |
| 3 | **Rio** | 200 | 민감 (Chain 3점) | 지속 DPS 1위 |
| 4 | **Sinobu** | 196 | 반환 (반사+막기) | 방어형 궁극기 |
| 5 | **Baran** | 184 | 일격분쇄 (Stun 3초) | Hard CC 포함 |
**핵심**:
- **Cazimord & Nave**: 둘 다 10.0배 궁극기, 상황별 최강
- Cazimord: 보스/단일 대상
- Nave: 몹 그룹/광역 상황
---
### 2. 유틸리티 분석 결과
#### 유틸리티 종합 점수
| 순위 | 스토커 | 점수 | 주요 유틸리티 |
|------|--------|------|---------------|
| 1 | **Clad** | 18점 | 힐 + DOT 제거 + 보호막 300 |
| 1 | **Rene** | 18점 | Lifesteal + 파티 흡혈 20초 |
| 3 | **Hilda** | 16점 | Blocking 100% + 도발 |
| 3 | **Sinobu** | 16점 | 기동성 + 궁극기 반사+막기 |
| 5 | **Cazimord** | 15점 | Parrying 생존 |
| 6 | **Baran** | 14점 | CC (갈고리, Stun 3초) |
| 6 | **Urud** | 14점 | CC (덫 Snare 3초) |
| 6 | **Lian** | 14점 | 궁극기 무제한 화살 |
| 9 | **Nave** | 13점 | 궁극기 광역 관통 |
| 9 | **Rio** | 13점 | 기동성 (돌진 4초) |
**DPS vs 유틸리티 트레이드오프**:
-**양호**: Clad (76 DPS, 18점), Rene (148, 18점), Cazimord (221, 15점)
- ⚠️ **불균형**: Rio (268, 13점) - 고DPS + 중유틸
---
### 3. 역할별 차별화
#### 전사 (3명)
- **Hilda**: 방어형 탱커 (Blocking, 도발) - DPS 117
- **Baran**: CC 특화 (Stun 3초 궁극기) - DPS 128
- **Cazimord**: 고숙련 DPS (Parrying, 평타 중심) - DPS 221, 버스트 256
**차별화**: 명확 ✅ (탱커/CC/DPS 역할 구분)
---
#### 원거리 (2명)
- **Urud**: CC 특화 (덫 Snare) + 궁극기 범위화 - DPS 82 ⚠️
- **Lian**: 고화력 (속사 4발, 만충전 1.5배) - DPS 219
**차별화**: 명확하나 격차 과다 ⚠️ (2.7배)
---
#### 마법사 (2명)
- **Nave**: 광역 폭딜 (화염구 2.0배, 궁극기 관통 10.0배) - DPS 202, 버스트 241
- **Rene**: 소환사 서포터 (정령, Lifesteal, 파티 흡혈) - DPS 148, 유틸 18점
**차별화**: 우수 ✅ (화력형/서포터형)
---
#### 암살자 (2명)
- **Rio**: DPS 특화 (압도적 268) - 짧은 쿨타임, Chain Score ⚠️
- **Sinobu**: 기동성 특화 (표창, 궁극기 반사+막기) - DPS 176, 유틸 16점
**차별화**: 명확하나 Rio 과다 ⚠️
---
#### 서포터 (1명)
- **Clad**: 유일한 힐러 (치유, DOT 제거, 보호막 300) - DPS 76, 유틸 18점
**평가**: 역할 완벽 ✅
---
### 4. 밸런스 이슈 및 개선안
#### ⚠️ 긴급 조정 필요
**1순위: Rio 너프**
- **문제**: DPS 268 (2위보다 +21% 과다)
- **원인**: 초짧은 쿨타임 (2.6~5.25초) + 높은 평타 DPS (196)
- **개선안**: 평타 배율 2.8 → 2.4 (-14%)
- **예상 DPS**: 238 (-30, -11%)
**2순위: Urud 버프**
- **문제**: DPS 82 (원거리 역할 붕괴)
- **원인**: Reload 페널티 + 낮은 스킬 배율
- **개선안**:
```
1. Reload: 2초 → 1.5초
2. 다발 화살: 1.2배 → 1.6배
3. 독침 화살: 0.8배 → 1.0배
```
- **예상 DPS**: 109 (+27, +33%)
---
#### 🔶 검토 단계
**Baran 개선 검토**
- **문제**: "파워 전사"인데 DPS 128 (중하위)
- **대안**: 컨셉 재정의 ("파워" → "CC 특화")
- **권장**: 현상 유지 (Stun 3초 궁극기로 차별화 충분)
---
### 5. 조정 후 예상 효과
#### DPS 순위 변화
| 순위 | 스토커 | 조정 전 | 조정 후 | 변화 |
|------|--------|---------|---------|------|
| 1 | Rio | 268 | **238** | ⬇️ -30 |
| 2 | Cazimord | 221 | 221 | - |
| 3 | Lian | 219 | 219 | - |
| 4 | Nave | 202 | 202 | - |
| 5 | Sinobu | 176 | 176 | - |
| 6 | Rene | 148 | 148 | - |
| 7 | Baran | 128 | 128 | - |
| 8 | Hilda | 117 | 117 | - |
| 9 | Urud | 82 | **109** | ⬆️ +27 |
| 10 | Clad | 76 | 76 | - |
#### 격차 개선
- **1위 vs 2위**: +21% → +8% (개선)
- **원거리 격차**: 2.7배 → 2.0배 (개선)
- **서포터 제외 최대 격차**: 3.3배 → 2.2배 (개선)
---
## 종합 평가
### 강점 ✅
1. **역할 다양성**: 5개 역할군 명확히 구분
2. **역할 내 차별화**: 전사 3종, 암살자 2종 등 차별화 우수
3. **궁극기 다양성**: 공격형/버프형/방어형 균형
4. **유틸리티 트레이드오프**: 대부분 양호 (Clad, Rene, Cazimord 등)
### 약점 ⚠️
1. **Rio DPS 과다**: 2위보다 +21%, 역할 다양성 위협
2. **Urud DPS 부족**: 원거리 역할 정체성 위협
3. **일부 격차 과다**: 원거리 2.7배, 전사 1.9배
4. **Rio Chain Score 시너지**: 짧은 쿨타임 + 높은 평타 과도한 조합
---
## 최종 권장사항
### Phase 1: 즉시 적용
1. **Rio 평타 배율 너프**: 2.8 → 2.4
2. **Urud 복합 버프**: Reload 단축 + 스킬 배율 증가
### Phase 2: 모니터링 (1개월 후)
1. Rio, Urud 채택률 변화 추적
2. 파티 구성 다양성 분석
3. Baran 추가 조정 여부 검토
### Phase 3: 장기 검토 (3개월 후)
1. 전체 밸런스 재평가
2. 궁극기 밸런스 (Nave vs Cazimord)
3. 신규 스토커 추가 시 기준 수립
---
## 밸런스 철학
**목표**:
- 모든 스토커가 상황에 따라 경쟁력 확보
- 역할 정체성 유지하면서 DPS 격차 완화
- 고숙련 보상 (Parrying) 유지
- 페널티 시스템 (Reload) 완화
**원칙**:
```
DPS + (유틸리티 × 15) = 270~290
```
**역할별 DPS 범위**:
- 암살자: 200~240
- 전사: 120~220 (역할별 다양)
- 마법사: 150~200
- 원거리: 100~220
- 서포터: 70~80
---
## 문서 구조
1. **01_요약.md** ← 현재 문서
2. **02_분석_전제조건.md**: 레벨, 장비, 룬 전제
3. **03_스토커별_기본데이터.md**: 10명 상세 스킬 정보
4. **04_DPS_계산_결과.md**: 평타/스킬/버스트 DPS 계산
5. **05_카지모르드_밸런스_검증.md**: Cazimord Parrying 시스템 분석
6. **06_유틸리티_평가.md**: CC/생존/기동/팀기여/궁극기 유틸
7. **07_역할별_차별화.md**: 5개 역할군 상세 비교
8. **08_밸런스_티어_및_개선안.md**: 최종 티어 및 구체적 수치 조정안
---
**분석자**: Claude (Anthropic)
**생성 일시**: 2025-10-24 02:30
**버전**: 1.0 (Nave 궁극기 수정 반영, 정확한 스킬 정보 기반)

View File

@ -0,0 +1,286 @@
# 02. 분석 전제조건
## 분석 대상
**10명의 스토커**:
1. Hilda (힐다) - 전사
2. Urud (우루드) - 원거리
3. Nave (나베) - 마법사
4. Baran (바란) - 전사
5. Rio (리오) - 암살자
6. Clad (클라드) - 성직자
7. Rene (레네) - 소환사
8. Sinobu (시노부) - 닌자
9. Lian (리안) - 레인저
10. **Cazimord (카지모르드) - 전사** ⭐ 신규 출시 예정
## 공통 설정
### 레벨 및 장비
- **레벨**: 20
- **기어스코어**: 400
- **플레이 숙련도**: 최적 플레이 (100% 활용)
### 장비 스탯 추정 (기어스코어 400 기준)
**무기** (레벨 20, Rare 등급 기준):
- PhysicalDamage: +65
- MagicalDamage: +65
**방어구 3부위** (갑옷, 다리, 액세서리):
- 총 PhysicalDamage: +15
- 총 MagicalDamage: +15
- HP: +120
- Defense: +80
**총 장비 보너스**:
- PhysicalDamage: +80
- MagicalDamage: +80
- HP: +120
- Defense: +80
## 룬 빌드 설정
### 룬 시스템 구조
**장착 방식**:
- Main 그룹 1개: Core, Sub1, Sub2 라인에서 각 1개씩 총 3개
- Sub 그룹 1개: Sub1, Sub2 라인에서 각 1개씩 총 2개
- **총 5개 룬 장착**
**룬 레벨**: 모두 Lv.5 (최대 레벨) 가정
### 역할별 최적 룬 빌드
#### 물리 딜러 (Hilda, Baran, Rio, Sinobu)
**Main: 스킬 그룹 (20xxx)**
- 20101 저주 (조건부 지연 피해)
- 20201 파괴 (+10% 스킬 피해)
- 20301 명상 (+70% 마나 회복)
**Sub: 전투 그룹 (10xxx)**
- 10201 분노 (+10% 물리 피해)
- 10103 공략 (+20% 머리 공격 피해)
**효과 요약**:
- 스킬 피해 +10%
- 물리 피해 +10%
- 머리 공격 +20%
- 마나 회복 +70%
#### 마법 딜러 (Nave, Rene)
**Main: 스킬 그룹 (20xxx)**
- 20103 활기 (마나 높을 때 스킬 피해 증가)
- 20202 왜곡 (-25% 쿨타임)
- 20301 명상 (+70% 마나 회복)
**Sub: 전투 그룹 (10xxx)**
- 10301 폭풍 (+10% 마법 피해)
- 10103 공략 (+20% 머리 공격 피해)
**효과 요약**:
- 스킬 쿨타임 -25%
- 마법 피해 +10%
- 머리 공격 +20%
- 마나 회복 +70%
- 조건부 스킬 피해 증가
#### 원거리 딜러 (Urud, Lian)
**Main: 스킬 그룹 (20xxx)**
- 20101 저주 (지연 피해)
- 20201 파괴 (+10% 스킬 피해)
- 20301 명상 (+70% 마나 회복)
**Sub: 전투 그룹 (10xxx)**
- 10201 분노 (+10% 물리 피해)
- 10103 공략 (+20% 머리 공격 피해)
**효과 요약**: 물리 딜러와 동일
#### 탱커/서포터 (Clad)
**Main: 전투 그룹 (10xxx)**
- 10101 충전 (+30% 궁극기 회복)
- 10202 방패 (+7% 물리 저항)
- 10302 수호 (+7% 마법 저항)
**Sub: 보조 그룹 (40xxx)**
- 40201 면역 (물약 사용 시 +20% 저항 20초)
- 40301 효율 (+50% 물약 효과)
**효과 요약**:
- 궁극기 회복 +30%
- 물리 저항 +7%
- 마법 저항 +7%
- 생존력 대폭 강화
#### 하이브리드 (Cazimord - 평타 중심)
**Main: 스킬 그룹 (20xxx)**
- 20101 저주 (지연 피해)
- 20202 왜곡 (-25% 쿨타임) ⭐ 패링 쿨감과 시너지
- 20301 명상 (+70% 마나 회복)
**Sub: 전투 그룹 (10xxx)**
- 10201 분노 (+10% 물리 피해)
- 10103 공략 (+20% 머리 공격 피해)
**효과 요약**:
- 스킬 쿨타임 -25% (패링과 중첩)
- 물리 피해 +10%
- 머리 공격 +20%
- 마나 회복 +70%
## 특수 시스템 활용률
**전제**: 최적 플레이 = 100% 활용
### 스토커별 특수 시스템
#### Hilda - Counter (반격)
- 반격 판정 구간: 0.5초 윈도우
- 활용률: 100% (적 공격 타이밍에 완벽 대응)
#### Urud & Lian - Reload
- 탄약: 6발
- 재장전 시간: 2.0초
- 활용률: 100% (탄약 관리 최적화)
#### Lian - Charging Bow
- **만충전 데미지: 1.5배** (정정됨, 2.0배 아님)
- 관통 효과: 없음 (정정됨)
- 충전 시간: 레벨당 0.5초 (최대 1.5초)
- 활용률: 100% (항상 만충전 후 발사)
#### Lian - Precision Aim
- 효과: 줌 + 명중률 증가
- 페널티: 이동 속도 감소
- 활용률: 원거리 정밀 타격 시 사용
#### Rio - Chain Score
- 최대 스택: 3
- **효과: 각 스킬별로 다른 위력 증가** (정정됨, +30% 통합 효과 아님)
- Dropping Attack 성공 시 스택 충전
- 활용률: 100% (항상 3스택 유지)
#### Rene - Spirit 소환 & Lifesteal
- Ifrit/Shiva 소환수
- Lifesteal: 피해량의 일정 % 회복
- 활용률: 100% (소환수 항상 활용)
#### Sinobu - Shuriken 충전
- 최대 충전: 3개
- 충전 속도: 1초/개
- 활용률: 100% (충전 관리 최적화)
#### Sinobu - Swap (위치 교환)
- 효과: 텔레포트
- 쿨타임: 11초
- 활용률: 전술적 포지셔닝
#### Cazimord - Flash 스택 ⭐
- **최대 스택: 2** (정정됨, 3스택 아님)
- Flash 스킬 사용 시 소모
- 충전: 수동 (전투 중 자연 회복)
- 활용률: 100% (2스택 유지)
#### Cazimord - Parrying (흘리기) ⭐
**메커니즘**:
- 패링 판정 구간: 0.2초
- 패링 성공 시:
- 적 피해 무효화
- 자동 반격 (높은 피해)
- **스킬 쿨타임 감소**:
- 섬광(SK170201): -3.8초
- 날개베기(SK170202): -3.8초
- 작열(SK170203): -6.8초
- **Flash 스택 충전 안 됨** (정정됨)
**활용률 시나리오**:
- **케이스 1: 패링 0%** (미사용)
- **케이스 2: 패링 100%** (완벽 성공)
**지구력 소모**:
- 패링 시도: 지구력 소모
- 패링 성공: 추가 소모
- 실패: 쿨타임 페널티
## 데이터 소스 규칙 (필수 준수)
### 1. 평타 몽타주
**소스**: `DT_CharacterAbility.attackMontageMap`
- 절대 다른 소스 사용 금지
- 게임에서 실제 사용되는 평타만 분석
### 2. 스킬 목록
**소스**: `DT_CharacterStat.defaultSkills`, `subSkill`, `ultimateSkill`
- 절대 다른 소스 사용 금지
- 어셋만 있고 실제 사용 안 하는 스킬 제외
### 3. 스킬 데이터
**소스**: `DT_Skill` (스킬 ID 기준 매칭)
- skillDamageRate: 기본 피해 배율
- coolTime: 쿨타임
- manaCost: 마나 소모
- skillAttackType: 공격 타입 (PhysicalSkill, MagicalSkill, Normal)
- skillElementType: 원소 타입
### 4. 애니메이션 타이밍
**소스**: `AnimMontage.json`
- SequenceLength: 애니메이션 지속 시간
- ANS_SkillCancel_C: 스킬 캔슬 윈도우
- AnimNotifyState_AttackWithEquip: 히트 판정 타이밍
### 5. Ability 로직
**소스**: `Blueprint.json`
- ActivationOrderGroup: 스킬 우선순위
- EventGraphs: 로직 구조
- Variables: 특수 변수 (스택, 쿨감 등)
## 분석 제외 사항
### 변수 제외
- 크리티컬: 확률 요소 제외 (평균 계산 복잡도)
- 던전 룰 배율: 기본 상태 기준
- 파티원 시너지: 개별 성능 중심
### 단순화 가정
- 머리/몸 피격: 머리 공격 70%, 몸 30% 비율 가정
- 정면/후면: 정면 공격 기준 (후면 배율 제외)
- 장비 옵션: 기본 고정 옵션만 (랜덤 옵션 제외)
## 분석 기준 시나리오
### 팀 플레이 (파티)
- 3인 파티 기준
- 역할 분담: 탱커 1 + 딜러 2 또는 딜러 3
- 시너지 고려
### PvE 던전
- 일반 몬스터 사냥 효율
- 생존력 (피해 감소 + 회복)
- 던전 클리어 시간
### 평가 지표
1. **DPS** (Damage Per Second)
- 평타 DPS
- 스킬 로테이션 DPS (30초)
- 버스트 DPS (10초 풀콤보)
2. **생존력**
- 유효 HP (HP × (1 + 방어/저항))
- 회복량 (힐, 라이프스틸)
- 회피/방어 메커니즘
3. **유틸리티**
- CC 능력 (지속시간/쿨타임)
- 기동성 (이동기, 대시)
- 팀 기여 (버프, 디버프)
---
**다음**: 03_스토커별_기본데이터.md

View File

@ -0,0 +1,562 @@
# 03. 스토커별 기본 데이터
## 데이터 소스
- `DT_CharacterStat`: 기본 스탯, 스킬 목록
- `DT_CharacterAbility`: 평타 몽타주
- `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 효과)
## 10명 스토커 종합 비교표
| 스토커 | 직업 | STR | DEX | INT | CON | WIS | 궁극기 보유 | 장착 가능 무기 | 평타 |
|--------|------|-----|-----|-----|-----|-----|-------------|----------------|------|
| Hilda | 전사 | 20 | 15 | 10 | 20 | 10 | ⭐ | WeaponShield | 3타 |
| Urud | 원거리 | 15 | 20 | 10 | 15 | 15 | ⭐ | Bow | 1타 |
| Nave | 마법사 | 10 | 10 | 25 | 10 | 20 | ⭐ | Staff | 2타 |
| Baran | 전사 | 25 | 10 | 5 | 25 | 10 | ⭐ | TwoHandWeapon | 3타 |
| Rio | 암살자 | 15 | 25 | 10 | 15 | 10 | ⭐ | ShortSword | 3타 |
| Clad | 성직자 | 15 | 10 | 10 | 20 | 20 | ⭐ | Mace | 2타 |
| Rene | 소환사 | 10 | 10 | 20 | 10 | 25 | ⭐ | Staff | 3타 |
| Sinobu | 닌자 | 10 | 25 | 10 | 15 | 15 | ⭐ | ShortSword | 2타 |
| Lian | 레인저 | 10 | 20 | 10 | 15 | 20 | ⭐ | Bow | 1타 |
| **Cazimord** | 전사 | 15 | 25 | 10 | 15 | 10 | ⭐ | WeaponShield | 3타 |
**특징**:
- **모든 스토커가 궁극기 보유**
- 모든 스토커 스탯 합계: 75 포인트 (균형)
- HP/MP 동일: 100/50
- 마나 회복: 0.2/초 (전원 동일)
---
## 궁극기 종합 비교
| 스토커 | 궁극기 이름 | 타입 | 주요 효과 | 지속/시전 |
|--------|-------------|------|-----------|-----------|
| **Hilda** | 마석 '핏빛 달' | Normal (버프) | 공격력 +15, 방어력 +25 | 20초 / 시전 2초 |
| **Urud** | 마석 '폭쇄' | Normal (버프) | 화살에 범위 피해 부여 + 30% 화상 | 15초 / 시전 2초 |
| **Nave** | 마석 '해방' | MagicalSkill | 관통 광선, 1.0배 마법 피해 | 5초 / 시전 2초 |
| **Baran** | 마석 '일격분쇄' | PhysicalSkill | 1.7배 물리 + 스턴 3초 + 광역 | 2초 / 시전 10초 |
| **Rio** | 마석 '민감' | Normal (버프) | 연계점수 3점 + 은신 + 투시 | 15초 / 시전 2초 |
| **Clad** | 마석 '황금' | Normal (버프) | 파티 보호막 300 생성 | 6초 / 시전 0.55초 |
| **Rene** | 마석 '붉은 축제' | MagicalSkill | 파티 공격에 흡혈 효과 | 20초 / 시전 2초 |
| **Sinobu** | 마석 '반환' | Normal (방어) | 투사체 반사 + 근접 막기 | 7초 / 시전 0초 |
| **Lian** | 마석 '폭우' | PhysicalSkill | 화살 무제한 + 쿨타임 감소 | 15초 / 시전 1.5초 |
| **Cazimord** | 마석 '칼날폭풍' | PhysicalSkill | 12회 연속 공격 (10×0.8 + 2×1.0) | 15초 / 시전 2초 |
**궁극기 타입 분류**:
- **버프형** (5명): Hilda, Urud, Rio, Clad, Rene - 팀 기여 및 생존력 강화
- **공격형** (3명): Nave, Baran, Cazimord - 직접 피해
- **유틸리티** (1명): Sinobu - 방어
- **하이브리드** (1명): Lian - 버프 + 공격 지원
---
## 1. Hilda (힐다) - 방어형 전사
### 기본 정보
- **역할**: 탱커
- **주 스탯**: STR 20, CON 20
- **특수 시스템**: Counter (반격, 0.5초 판정 윈도우)
- **평타**: WeaponShield 3타 콤보
### 스킬 목록
**기본 스킬**:
1. **SK100201 칼날 찌르기** (Sword Strike)
- 타입: PhysicalSkill / Lightning 속성
- 피해 배율: 1.3
- 쿨타임: 6초 / 마나: 11
2. **SK100202 반격** (Counter)
- 타입: PhysicalSkill
- 피해 배율: 1.2
- 쿨타임: 4초 / 마나: 10
- 특수: 0.5초 반격 판정 윈도우, 피해 무효 + 반격
3. **SK100204 도발** (Provoke)
- 타입: Normal (유틸리티)
- 쿨타임: 10초 / 마나: 8
- 효과: 어그로 유도
**서브 스킬**:
- **SK100101 방패 들기** (Blocking)
- 타입: Normal (토글)
- 쿨타임: 0초
- 효과: 정면 물리 피해 100% 차단, 마법 90% 차단
**궁극기**:
- **SK100301 마석 '핏빛 달'**
- 타입: Normal (버프)
- 지속시간: 20초 / 시전: 2초
- 효과: 공격력 +15, 방어력 +25
- 특징: 장시간 버프로 탱킹 + DPS 향상
---
## 2. Urud (우르드) - 원거리 딜러
### 기본 정보
- **역할**: 원거리 물리 딜러
- **주 스탯**: DEX 20, WIS 15
- **특수 시스템**: Reload (탄약 6발, 재장전 2초)
- **평타**: Bow 1타 (반복)
### 스킬 목록
**기본 스킬**:
1. **SK110205 다발 화살** (Multi Shot)
- 타입: PhysicalSkill
- 피해 배율: 1.2 (다수 타격)
- 쿨타임: 7초 / 마나: 14
2. **SK110204 독침 화살** (Poison Arrow)
- 타입: PhysicalSkill / Poison 속성
- 피해 배율: 0.8 + DOT
- 쿨타임: 7초 / 마나: 9
3. **SK110201 덫 설치** (Make Trap)
- 타입: Normal
- 쿨타임: 5초 / 마나: 9
- 효과: Snare (속박 3초)
4. **SK110207 Reload**
- 타입: Normal
- 쿨타임: 0초 / 마나: 0
- 효과: 탄약 6발 재장전 (2초 소요)
**서브 스킬**:
- **SK110101 화살 발사** (평타)
- 타입: PhysicalSkill
- 쿨타임: 0초
**궁극기**:
- **SK110301 마석 '폭쇄'**
- 타입: Normal (버프)
- 지속시간: 15초 / 시전: 2초
- 효과: **화살에 범위 피해 부여 (무조건 스플래시) + 30% 확률 화상**
- GE 정보: Data.Value: 0.3, Data.SkillRate: 0.3 (스플래시 30% 피해)
- 특징: 모든 화살이 범위 공격화
---
## 3. Nave (네이브) - 마법사
### 기본 정보
- **역할**: 광역 마법 딜러
- **주 스탯**: INT 25, WIS 20
- **특수 시스템**: 없음
- **평타**: Staff 2타 콤보
### 스킬 목록
**기본 스킬**:
1. **SK120201 마법 화살**
- 타입: MagicalSkill
- 피해 배율: 0.8
- 쿨타임: 3.5초 / 마나: 18
- 설명: 3개의 마법 화살을 생성하여 발사
2. **SK120202 화염구**
- 타입: MagicalSkill / Fire 속성
- 피해 배율: 2.0
- 쿨타임: 5초 / 마나: 25
- 설명: 화염구를 생성하여 발사, 주변에 추가 피해
3. **SK120206 노대바람**
- 타입: MagicalSkill
- 피해 배율: 0.5
- 쿨타임: 7초 / 마나: 9
- 설명: 강한 바람으로 적을 밀쳐내며 피해
**서브 스킬**:
- **SK120101 마력 충전**
- 타입: Normal
- 쿨타임: 1초
- 효과: 시전하는 동안 마나를 추가로 회복
**궁극기**:
- **SK120301 마석 '해방'** (Liberation)
- 타입: MagicalSkill
- 피해 배율: 1.0 × 10회 = **총 10.0배**
- 지속시간: 5초 / 시전: 2초
- Tick: 0.5초마다 / Count: 10회
- 효과: **적을 관통하는 직선 광선 발사, 0.5초마다 피해 (10회 연속)**
- 특징: 직선상 모든 적에게 10회 연속 타격, **Cazimord와 동급의 초강력 광역 궁극기**
- 참고: DT_Skill Active->Range 필드에 Tick=0.5, Count=10 정의
---
## 4. Baran (바란) - 파워 전사
### 기본 정보
- **역할**: 고화력 전사
- **주 스탯**: STR 25, CON 25
- **특수 시스템**: 없음
- **평타**: TwoHandWeapon 3타 콤보
### 스킬 목록
**기본 스킬**:
1. **SK130204 갈고리 투척**
- 타입: PhysicalSkill
- 피해 배율: 0.25
- 쿨타임: 13초 / 마나: 14
- 설명: 갈고리를 던져 피해, 적중된 대상 끌어당김 + 경직
2. **SK130203 후려치기**
- 타입: PhysicalSkill
- 피해 배율: 1.2
- 쿨타임: 8초 / 마나: 9
- 설명: 대검을 크게 휘둘러 두 번 연속 피해
3. **SK130206 깊게 찌르기**
- 타입: PhysicalSkill
- 피해 배율: 1.1
- 쿨타임: 7초 / 마나: 10
- 설명: 대검을 깊게 찔러 피해, 문 파괴 가능 + 경직
**서브 스킬**:
- **SK130101 무기 막기**
- 타입: Normal
- 쿨타임: 0초
- 효과: 무기로 공격 방어, 지구력 소모
**궁극기**:
- **SK130301 마석 '일격분쇄'**
- 타입: PhysicalSkill
- 피해 배율: 1.7
- 지속시간: 2초 / 시전: 10초
- 효과: **고피해 물리 공격 + 스턴 3초 + 광역 피해**
- GE 정보: Data.Duration: 3 (스턴)
- 특징: 최고 피해 배율 궁극기
---
## 5. Rio (리오) - 암살자
### 기본 정보
- **역할**: 빠른 근접 암살자
- **주 스탯**: DEX 25, STR 15
- **특수 시스템**: Chain Score (최대 3스택, 스킬별 추가 효과)
- **평타**: ShortSword 3타 콤보 (빠름)
### 스킬 목록
**기본 스킬**:
1. **SK140201 연속 찌르기**
- 타입: PhysicalSkill
- 피해 배율: 1.0
- 쿨타임: 3.5초 / 마나: 9
- 설명: 단검을 빠르게 2번 찔러 피해, 각 공격은 추가 치명타 확률
2. **SK140205 접근**
- 타입: Normal
- 피해 배율: 1.0
- 쿨타임: 4초 / 마나: 8
- 설명: 낮은 자세로 돌진, 돌진 중 피격 무효, 돌진 후 피해 증가
3. **SK140202 단검 투척**
- 타입: PhysicalSkill
- 피해 배율: 1.0
- 쿨타임: 7초 / 마나: 10
- 설명: 단검을 던져 피해
**서브 스킬**:
- **SK140101 내려 찍기**
- 타입: PhysicalSkill
- 쿨타임: 0초
- 설명: 단검으로 내려 찍어 피해, 연계 점수에 따라 추가 피해
**궁극기**:
- **SK140301 마석 '민감'** (Sensitive)
- 타입: Normal (버프)
- 피해 배율: 0.3
- 지속시간: 15초 / 시전: 2초
- 효과: **연계점수 3점 즉시 획득 + 은신 + 투시 + 약점 판정**
- 특징: Chain Score 3스택 즉시 충전, 후방 공격 시 추가 피해
---
## 6. Clad (클라드) - 성직자
### 기본 정보
- **역할**: 서포터 / 힐러
- **주 스탯**: CON 20, WIS 20
- **특수 시스템**: 없음
- **평타**: Mace 2타 콤보
### 스킬 목록
**기본 스킬**:
1. **SK150206 치유**
- 타입: MagicalSkill (힐)
- 피해 배율: 1.0
- 쿨타임: 3초 / 마나: 12
- 설명: 대상의 체력을 회복, 대상이 없을 경우 자신에게 시전
2. **SK150201 다시 흙으로**
- 타입: MagicalSkill / Holy 속성
- 피해 배율: 1.5
- 쿨타임: 5초 / 마나: 9
- 설명: 범위 내의 적에게 피해
3. **SK150202 신성한 빛**
- 타입: MagicalSkill
- 피해 배율: 0 (유틸리티)
- 쿨타임: 7.5초 / 마나: 15
- 설명: 주변 아군의 지속 피해 효과 제거
**서브 스킬**:
- **SK150101 방패 방어**
- 타입: Normal
- 쿨타임: 0초
- 효과: 방패로 공격 방어, 지구력 소모
**궁극기**:
- **SK150301 마석 '황금'** (Gold Shield)
- 타입: Normal (버프)
- skillDamageRate: 300 (보호막 수치)
- 지속시간: 6초 / 시전: 0.55초
- 효과: **자신과 아군에게 보호막 300 생성**
- 특징: 힐이 아닌 보호막 (피해 흡수)
---
## 7. Rene (레네) - 소환사
### 기본 정보
- **역할**: 소환사 / 마법 딜러
- **주 스탯**: INT 20, WIS 25
- **특수 시스템**: Spirit 소환 (Ifrit, Shiva), Lifesteal
- **평타**: Staff 3타 콤보
### 스킬 목록
**기본 스킬**:
1. **SK160202 정령 소환: 화염**
- 타입: MagicalSkill
- 피해 배율: 1.2
- 쿨타임: 7초 / 마나: 8
- 설명: 화염 화살을 발사하는 화염의 정령을 소환, 정령은 이동하지 않음
2. **SK160206 정령 소환: 냉기**
- 타입: MagicalSkill
- 피해 배율: 0.8
- 쿨타임: 10초 / 마나: 15
- 설명: 얼음 송곳을 발사하는 냉기의 정령을 소환, 정령은 레네를 따라 이동
3. **SK160203 독기 화살**
- 타입: MagicalSkill / Dark 속성
- 피해 배율: 1.0
- 쿨타임: 10초 / 마나: 15
- 설명: 방어력 무시 피해, 적중 시 출혈 상태 부여
**서브 스킬**:
- **SK160101 할퀴기**
- 타입: MagicalSkill
- 쿨타임: 0초
- 설명: 손톱을 휘둘러 피해, 피해량의 일정 수치 회복 (자체 흡혈)
**궁극기**:
- **SK160301 마석 '붉은 축제'** (Blood Carnival)
- 타입: MagicalSkill
- skillDamageRate: 50
- 지속시간: 20초 / 시전: 2초
- 효과: **자신과 아군의 모든 공격에 흡혈 효과 부여 (피해의 일정 % 회복)**
- 특징: 파티 생존력 대폭 향상
---
## 8. Sinobu (시노부) - 닌자
### 기본 정보
- **역할**: 기동형 암살자
- **주 스탯**: DEX 25, CON 15
- **특수 시스템**: Shuriken 충전 (최대 3개, 1초/개), Swap (텔레포트)
- **평타**: ShortSword 2타 콤보
### 스킬 목록
**기본 스킬**:
1. **SK180202 기폭찰**
- 타입: PhysicalSkill
- 피해 배율: 1.3
- 쿨타임: 6초 / 마나: 10
- 설명: 뒤로 점프하며 기폭찰 쿠나이 설치, 적 접근 시 폭발
2. **SK180203 비뢰각**
- 타입: PhysicalSkill / Lightning 속성
- 피해 배율: 1.1
- 쿨타임: 8초 / 마나: 11
- 설명: 대각선 날아차기, 공중 전용, 적중 시 경직
3. **SK180205 인술 '바꿔치기'**
- 타입: PhysicalSkill
- 피해 배율: 0.9
- 쿨타임: 11초 / 마나: 12
- 설명: 사용 후 피격 시 피해 감소 + 투명화 + 이동속도 증가
**서브 스킬**:
- **SK180101 표창** (Shuriken)
- 타입: PhysicalSkill
- 피해 배율: 0.8
- 쿨타임: 0초 (충전 시스템)
- 특수: 최대 3개 충전, 1초/개
**궁극기**:
- **SK180301 마석 '반환'** (Deflect)
- 타입: Normal (방어)
- skillDamageRate: 0 (피해 없음)
- 지속시간: 7초 / 시전: 0초
- 효과: **전방 투사체 튕겨내기 + 근접 공격 막기**
- 특징: 방어형 궁극기, 피해 무효화
---
## 9. Lian (리안) - 레인저
### 기본 정보
- **역할**: 정밀 원거리 딜러
- **주 스탯**: DEX 20, WIS 20
- **특수 시스템**: Reload (탄약 6발, 2초), Charging Bow (만충전 1.5배)
- **평타**: Bow 1타 (만충전 필수)
### 스킬 목록
**기본 스킬**:
1. **SK190207 속사**
- 타입: PhysicalSkill
- 피해 배율: 0.85
- 쿨타임: 7초 / 마나: 16
- 설명: 4발의 화살을 빠르게 발사
2. **SK190205 비연사**
- 타입: PhysicalSkill
- 피해 배율: 1.5
- 쿨타임: 10초 / 마나: 15
- 설명: 뒤로 빠지며 화살 발사
3. **SK190201 연화**
- 타입: PhysicalSkill / Holy 속성
- 피해 배율: 1.2
- 쿨타임: 7.5초 / 마나: 12
- 설명: 적 추적하는 연꽃 생성 발사, 적중 시 피해 감소 디버프
4. **SK190209 재장전**
- 타입: Normal
- 쿨타임: 0초
- 설명: 탄약 재장전 (6발)
**서브 스킬**:
- **SK190101 정조준**
- 타입: PhysicalSkill
- 쿨타임: 0초
- 설명: 조준 중 피해량 증가, 이동 속도 감소
**궁극기**:
- **SK190301 마석 '폭우'** (Arrow Rain)
- 타입: PhysicalSkill
- skillDamageRate: 50
- 지속시간: 15초 / 시전: 1.5초
- 효과: **화살 무제한 (재장전 불필요) + 스킬 쿨타임 감소**
- 특징: Reload 페널티 제거, 지속 DPS 극대화
---
## 10. Cazimord (카지모르드) - 평타 중심 전사 ⭐
### 기본 정보
- **역할**: 고숙련도 하이브리드 전사
- **주 스탯**: DEX 25, STR 15
- **특수 시스템**:
- Parrying (흘리기, 0.2초 판정)
- Flash 스택 (최대 2스택)
- **평타**: WeaponShield 3타 콤보 (빠름, 마지막 타격 강화)
- **설계 의도**: 평타 중심, 높은 스킬 캡
### 스킬 목록
**기본 스킬**:
1. **SK170201 섬광** (Flash)
- 타입: PhysicalSkill
- 피해 배율: 0.5
- 쿨타임: 15.5초 / 마나: 9
- 특수: Flash 스택 1개 소모, 빠른 대시 공격
- **최대 스택**: 2개
2. **SK170202 날개베기** (Blade Storm)
- 타입: PhysicalSkill
- 피해 배율: 0.3
- 쿨타임: 15.5초 / 마나: 13
- 특수: 회전 범위 공격
3. **SK170203 작열** (Burn)
- 타입: Normal (버프)
- 쿨타임: 27.5초 / 마나: 10
- 효과: +20% 피해, 10초 지속
- 특수: 평타/스킬 모두 강화
**서브 스킬**:
- **SK170101 Parrying** (흘리기)
- 타입: Normal (방어)
- 쿨타임: 지구력 소모
- 판정 윈도우: **0.2초** (Hilda Counter 0.5초보다 2.5배 짧음)
- 성공 시 효과:
- 피해 무효화
- 자동 반격 (높은 피해)
- **스킬 쿨타임 감소**:
- 섬광 (Flash): -3.8초
- 날개베기 (Blade Storm): -3.8초
- 작열 (Burn): -6.8초
- ❌ Flash 스택 충전 안 됨
**궁극기**:
- **SK170301 마석 '칼날폭풍'** (Blade Storm Ultimate)
- 타입: PhysicalSkill
- 피해 배율: 0.8 (처음 10회), 1.0 (마지막 2회)
- 지속시간: 15초 / 시전: 2초
- 효과: **12회 연속 빠른 공격**
- 1~10타: 각 0.8배 물리 피해
- 11~12타: 각 1.0배 물리 피해
- 총 피해: 0.8×10 + 1.0×2 = **10.0배**
- 특징: 시전 중 이동 불가, 정면 집중 공격
### Cazimord Parrying 시스템 상세
**패링 0% vs 100% 비교** (왜곡 룬 -25% 쿨타임 포함):
| 스킬 | 기본 쿨타임 | 왜곡 룬 적용 | 패링 감소 | 최종 쿨타임 (100%) | 30초 사용 횟수 |
|------|-------------|--------------|-----------|-------------------|----------------|
| 섬광 (Flash) | 15.5초 | 11.6초 | -3.8초 | **7.8초** | 3회 (0%: 2회) |
| 날개베기 (Blade) | 15.5초 | 11.6초 | -3.8초 | **7.8초** | 3회 (0%: 2회) |
| 작열 (Burn) | 27.5초 | 20.6초 | -6.8초 | **13.8초** | 2회 (0%: 1회) |
**패링 활용률**:
- **패링 0%**: 스킬 중심 플레이, Burn 버프 33% 가동률
- **패링 100%**: 평타 중심 플레이, Burn 버프 67% 가동률, 스킬 회전율 2배
---
## 특수 시스템 요약
| 스토커 | 특수 시스템 | 메커니즘 |
|--------|-------------|----------|
| **Hilda** | Counter | 0.5초 판정, 피해 무효 + 반격 |
| **Urud** | Reload | 6발, 2초 재장전 |
| **Nave** | - | - |
| **Baran** | - | - |
| **Rio** | Chain Score | 최대 3스택, 스킬별 추가 효과 |
| **Clad** | - | - |
| **Rene** | Spirit 소환 + Lifesteal | Ifrit/Shiva, 흡혈 |
| **Sinobu** | Shuriken 충전 + Swap | 3개, 1초/개 + 텔레포트 |
| **Lian** | Reload + Charging Bow | 6발, 2초 + 만충전 1.5배 |
| **Cazimord** | Parrying + Flash 스택 | 0.2초 판정 + 쿨타임 감소 + 2스택 |
---
**다음**: 04_DPS_계산_결과.md - 궁극기 포함 DPS 재계산
---
**생성 일시**: 2025-10-24 02:00
**데이터 소스**: DT_CharacterStat, DT_CharacterAbility, DT_Skill (정정 완료)

View File

@ -0,0 +1,780 @@
# 04. DPS 계산 결과
## 계산 방법론
### 1. BaseDamage 계산식
```
BaseDamage = (기본스탯 + 장비보너스) × (1 + 룬보너스%) × 레벨배율
```
**레벨 20 기준**:
- 레벨배율: 1.0 (기준)
- 장비보너스: +80 (Physical/Magical)
- 룬보너스: 역할별로 상이 (02_분석_전제조건 참고)
### 2. DPS 계산 유형
#### 평타 DPS (Basic Attack DPS)
```
평타 DPS = (평타 총 피해량) / (평타 콤보 총 시간)
```
- 평타 콤보: DT_CharacterAbility.attackMontageMap 기준
- 타이밍: AnimMontage 지속시간 기준
- 특수 시스템 반영 (Reload, Charging 등)
#### 스킬 로테이션 DPS (30초)
```
스킬 로테이션 DPS = (30초간 총 피해량) / 30초
```
- 스킬 우선순위: 쿨타임 짧은 순서
- 마나 관리: 마나 회복 (+70% 룬) 반영
- 평타 필러: 스킬 쿨타임 중 평타 사용
#### 버스트 DPS (10초)
```
버스트 DPS = (궁극기 포함 풀콤보 피해량) / 10초
```
- 궁극기 + 모든 스킬 + 평타
- 최대 화력 순간
- 마나 제한 무시
### 3. 데미지 타입별 BaseDamage
#### 물리 딜러 (Physical DPS)
```
Physical BaseDamage = (STR or DEX + 80) × 1.20
```
- 룬 효과: +10% 물리 피해 + +10% 스킬 피해 = 1.20
**적용 대상**: Hilda, Baran, Rio, Urud, Sinobu, Lian, Cazimord
#### 마법 딜러 (Magical DPS)
```
Magical BaseDamage = (INT + 80) × 1.10
```
- 룬 효과: +10% 마법 피해 = 1.10
**적용 대상**: Nave, Rene
#### 탱커/서포터
```
BaseDamage = (주스탯 + 80) × 1.00
```
- 룬 효과: 생존력 중심 (피해 증가 룬 없음)
**적용 대상**: Clad
---
## 10명 스토커 BaseDamage 비교
### 물리 딜러
| 스토커 | 주 스탯 | 스탯값 | 장비 | 룬배율 | **Physical BaseDamage** |
|--------|---------|--------|------|--------|-------------------------|
| Hilda | STR | 20 | +80 | ×1.20 | **120** |
| Baran | STR | 25 | +80 | ×1.20 | **126** |
| Rio | DEX | 25 | +80 | ×1.20 | **126** |
| Urud | DEX | 20 | +80 | ×1.20 | **120** |
| Sinobu | DEX | 25 | +80 | ×1.20 | **126** |
| Lian | DEX | 20 | +80 | ×1.20 | **120** |
| Cazimord | DEX | 25 | +80 | ×1.20 | **126** |
### 마법 딜러
| 스토커 | 주 스탯 | 스탯값 | 장비 | 룬배율 | **Magical BaseDamage** |
|--------|---------|--------|------|--------|-------------------------|
| Nave | INT | 25 | +80 | ×1.10 | **115.5** |
| Rene | INT | 20 | +80 | ×1.10 | **110** |
### 탱커/서포터
| 스토커 | 주 스탯 | 스탯값 | 장비 | 룬배율 | **BaseDamage** |
|--------|---------|--------|------|--------|----------------|
| Clad | STR | 15 | +80 | ×1.00 | **95** |
**분석**:
- **최고 Physical BaseDamage**: Baran, Rio, Sinobu, Cazimord (126)
- **최고 Magical BaseDamage**: Nave (115.5)
- **BaseDamage 격차**: 최대 33% (Cazimord 126 vs Clad 95)
---
## 평타 DPS 분석
### 평타 콤보 구조
| 스토커 | 무기 | 콤보 | 평타 배율 합계 | 예상 콤보 시간 | 평타 DPS |
|--------|------|------|----------------|----------------|----------|
| Hilda | WeaponShield | 3타 | 1.0 + 1.0 + 1.0 = 3.0 | ~2.5초 | **144** |
| Urud | Bow | 1타 (반복) | 1.0 | ~1.0초 | **120** |
| Nave | Staff | 2타 | 1.0 + 1.0 = 2.0 | ~2.0초 | **115.5** |
| Baran | TwoHandWeapon | 3타 | 1.2 + 1.2 + 1.5 = 3.9 | ~3.0초 | **164** |
| Rio | ShortSword | 3타 | 0.8 + 0.8 + 1.2 = 2.8 | ~1.8초 | **196** |
| Clad | Mace | 2타 | 1.0 + 1.0 = 2.0 | ~2.2초 | **86** |
| Rene | Staff | 3타 | 1.0 + 1.0 + 1.0 = 3.0 | ~2.5초 | **132** |
| Sinobu | ShortSword | 2타 | 0.8 + 1.0 = 1.8 | ~1.5초 | **151** |
| Lian | Bow | 1타 (만충전) | 1.5 | ~2.0초 | **90** |
| Cazimord | WeaponShield | 3타 | 1.0 + 1.0 + 1.2 = 3.2 | ~2.2초 | **184** |
**계산 방식**:
```
평타 DPS = (BaseDamage × 평타배율합계) / 콤보시간
```
**예시 (Rio)**:
```
Rio 평타 DPS = (126 × 2.8) / 1.8초 = 196 DPS
```
### 특수 시스템 반영
#### Urud & Lian - Reload 시스템
- **Urud**: 6발 발사 후 2초 재장전
- 평균 DPS = 120 × (6 / (6 + 2)) = **90 DPS** (재장전 고려)
#### Lian - Charging Bow
- **만충전 피해**: 1.5배
- **충전 시간**: 1.5초 + 발사 0.5초 = 2.0초
```
Lian 평타 DPS = (120 × 1.5) / 2.0초 = 90 DPS
```
- **재장전 고려**: 6발 발사 후 2초
- 평균 DPS = 90 × (12 / (12 + 2)) ≈ **77 DPS**
#### Rio - Chain Score 3스택
- **효과**: 각 스킬별 추가 효과 (스킬 섹션에서 상세 분석)
- 평타 자체는 3스택 유지 전제로 계산
#### Cazimord - Flash 스택
- Flash 스택은 스킬 사용 시 소모
- 평타 DPS에는 직접 영향 없음
### 평타 DPS 순위
| 순위 | 스토커 | 평타 DPS | 특징 |
|------|--------|----------|------|
| 1 | **Rio** | **196** | 빠른 3타 콤보, Chain Score |
| 2 | **Cazimord** | **184** | 빠른 3타, 마지막 타격 강화 |
| 3 | **Baran** | **164** | 높은 배율, 느린 공격 |
| 4 | **Sinobu** | **151** | 빠른 2타 콤보 |
| 5 | **Hilda** | **144** | 균형잡힌 3타 |
| 6 | **Rene** | **132** | 마법사 평타 |
| 7 | **Urud** | **90** | 재장전 페널티 |
| 8 | **Lian** | **77** | 충전 + 재장전 페널티 |
| 9 | **Nave** | **115.5** | 마법사 평타 |
| 10 | **Clad** | **86** | 서포터, 낮은 BaseDamage |
**결론**:
- **암살자 Rio**가 압도적 평타 DPS (196)
- **신규 Cazimord** 2위 (184), 설계 의도와 일치
- **원거리 Reload 조합** (Urud, Lian)은 재장전으로 평타 DPS 최하위권
---
## 스킬 로테이션 DPS (30초)
### 계산 전제
- 30초 동안 반복 가능한 모든 스킬 사용
- 마나 회복: +70% 룬 반영 (마나 부족 없음)
- 스킬 쿨타임 중 평타 필러 사용
- 특수 시스템 100% 활용
### 주요 스토커 스킬 로테이션 분석
#### 1. Hilda (힐다) - 방어형 전사
**스킬 목록**:
1. Sword Strike (칼날 찌르기): 1.3배, 6초 쿨
2. Counter (반격): 1.2배, 4초 쿨
3. Provoke (도발): 유틸리티
**30초 로테이션**:
```
Counter(1.2) → Sword Strike(1.3) → Counter(1.2) → Sword Strike(1.3) → Counter(1.2) → Sword Strike(1.3) → Counter(1.2) → Sword Strike(1.3) → Counter(1.2)
+ 평타 필러 (약 15초)
```
**총 피해량**:
- Counter: 5회 × (120 × 1.2) = 720
- Sword Strike: 4회 × (120 × 1.3) = 624
- 평타 필러: (144 DPS × 15초) = 2,160
- **총합**: 3,504 피해
**스킬 로테이션 DPS**: 3,504 / 30초 = **117 DPS**
---
#### 2. Urud (우루드) - 원거리 딜러
**스킬 목록**:
1. Multi Shot (다발 화살): 1.2배, 7초 쿨
2. Poison Arrow (독침 화살): 0.8배 + DOT, 7초 쿨
3. Make Trap (덫): 유틸리티
**30초 로테이션**:
```
Multi Shot(1.2) → Poison Arrow(0.8+DOT) → 평타 → Multi Shot → Poison Arrow → ...
+ Reload 4회 (각 2초)
```
**총 피해량**:
- Multi Shot: 4회 × (120 × 1.2) = 576
- Poison Arrow: 4회 × (120 × 0.8) = 384
- DOT: 4회 × (120 × 0.5) = 240 (추정)
- 평타 필러: (90 DPS × 14초) = 1,260
- **총합**: 2,460 피해
**스킬 로테이션 DPS**: 2,460 / 30초 = **82 DPS**
---
#### 3. Nave (네이브) - 마법사
**스킬 목록**:
1. SK120201 마법 화살: 0.8배, 3.5초 쿨 (3개 발사)
2. SK120202 화염구: 2.0배, 5초 쿨 (Fire 속성, 범위 피해)
3. SK120206 노대바람: 0.5배, 7초 쿨 (넉백)
4. 궁극기: SK120301 마석 '해방' (관통 광선, 1.0배, 5초 지속)
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- 마법 화살: 2.6초
- 화염구: 3.75초
- 노대바람: 5.25초
**30초 로테이션**:
```
화염구(2.0) → 마법 화살(0.8×3) → 노대바람(0.5) → 화염구(2.0) → 마법 화살(0.8×3) → ...
+ 평타 필러 (약 12초)
```
**총 피해량**:
- 화염구: 7회 × (115.5 × 2.0) = 1,617
- 마법 화살: 10회 × (115.5 × 0.8 × 3) = 2,772 (3개씩)
- 노대바람: 5회 × (115.5 × 0.5) = 289
- 평타 필러: (115.5 DPS × 12초) = 1,386
- **총합**: 6,064 피해
**스킬 로테이션 DPS**: 6,064 / 30초 = **202 DPS**
---
#### 4. Baran (바란) - 파워 전사
**스킬 목록**:
1. SK130204 갈고리 투척: 0.25배, 13초 쿨 (끌어당김 + 경직)
2. SK130203 후려치기: 1.2배, 8초 쿨 (2연타)
3. SK130206 깊게 찌르기: 1.1배, 7초 쿨 (문 파괴 + 경직)
4. 궁극기: SK130301 마석 '일격분쇄' (1.7배, 3초 스턴)
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- 갈고리 투척: 9.75초
- 후려치기: 6초
- 깊게 찌르기: 5.25초
**30초 로테이션**:
```
갈고리(0.25) → 후려치기(1.2) → 깊게 찌르기(1.1) → 후려치기 → 깊게 찌르기 → 갈고리 → 후려치기 → ...
+ 평타 필러 (약 14초)
```
**총 피해량**:
- 갈고리 투척: 3회 × (126 × 0.25) = 95
- 후려치기: 5회 × (126 × 1.2) = 756
- 깊게 찌르기: 5회 × (126 × 1.1) = 693
- 평타 필러: (164 DPS × 14초) = 2,296
- **총합**: 3,840 피해
**스킬 로테이션 DPS**: 3,840 / 30초 = **128 DPS**
---
#### 5. Rio (리오) - 암살자
**스킬 목록**:
1. SK140201 연속 찌르기: 1.0배, 3.5초 쿨 (2회 연타, 치명타 확률 증가)
2. SK140205 접근: 1.0배, 4초 쿨 (돌진, 피격 무효, 피해 증가)
3. SK140202 단검 투척: 1.0배, 7초 쿨
4. 서브 SK140101 내려 찍기: Chain Score에 따라 추가 피해
5. 궁극기: SK140301 마석 '민감' (Chain Score 3점 + 은신 + 투시)
**Chain Score 효과** (3스택 기준):
- 내려 찍기: 추가 피해 +30% (추정)
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- 연속 찌르기: 2.6초
- 접근: 3초
- 단검 투척: 5.25초
**30초 로테이션**:
```
연속 찌르기(1.0×2) → 접근(1.0) → 단검 투척(1.0) → 연속 찌르기 → 접근 → ...
+ 평타 필러 (약 18초)
+ 내려 찍기 (Chain Score 활용)
```
**총 피해량**:
- 연속 찌르기: 10회 × (126 × 1.0 × 2) = 2,520 (2연타)
- 접근: 9회 × (126 × 1.0 × 1.2) = 1,361 (피해 증가 +20% 추정)
- 단검 투척: 5회 × (126 × 1.0) = 630
- 평타 필러: (196 DPS × 18초) = 3,528
- **총합**: 8,039 피해
**스킬 로테이션 DPS**: 8,039 / 30초 = **268 DPS**
---
#### 6. Clad (클라드) - 성직자
**스킬 목록**:
1. SK150206 치유: 1.0배 (힐링), 3초 쿨
2. SK150201 다시 흙으로: 1.5배, 5초 쿨 (Holy 속성, 범위 공격)
3. SK150202 신성한 빛: 유틸리티 (DOT 제거), 7.5초 쿨
4. 궁극기: SK150301 마석 '황금' (파티 보호막 300)
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- 치유: 2.25초
- 다시 흙으로: 3.75초
- 신성한 빛: 5.6초
**30초 로테이션**:
```
다시 흙으로(1.5) → 치유 → 다시 흙으로 → 치유 → 신성한 빛 (DOT 제거) → ...
+ 평타 필러 (약 15초)
+ 치유 스킬 (파티원 힐)
```
**총 피해량**:
- 다시 흙으로: 7회 × (95 × 1.5) = 998
- 치유: DPS 미포함 (힐링 목적)
- 신성한 빛: DPS 미포함 (유틸리티)
- 평타 필러: (86 DPS × 15초) = 1,290
- **총합**: 2,288 피해
**스킬 로테이션 DPS**: 2,288 / 30초 = **76 DPS**
---
#### 7. Rene (레네) - 소환사
**스킬 목록**:
1. SK160202 정령 소환: 화염: 1.2배 + 소환수, 7초 쿨
2. SK160206 정령 소환: 냉기: 0.8배 + 소환수, 10초 쿨
3. SK160203 독기 화살: 1.0배, 10초 쿨 (Dark, 방어력 무시, 출혈)
4. 서브 SK160101 할퀴기: 자체 흡혈
5. 궁극기: SK160301 마석 '붉은 축제' (파티 흡혈 효과, 20초)
**소환수 공격**:
- 화염 정령: 80 DPS (10초 지속, 추정)
- 냉기 정령: 60 DPS (10초 지속, 추정)
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- 정령 소환: 화염: 5.25초
- 정령 소환: 냉기: 7.5초
- 독기 화살: 7.5초
**30초 로테이션**:
```
화염 정령(1.2) → 독기 화살(1.0) → 냉기 정령(0.8) → 독기 화살 → 화염 정령 → ...
+ 소환수 공격 (화염+냉기 동시)
+ 평타 필러 (약 12초)
```
**총 피해량**:
- 화염 정령: 5회 × (110 × 1.2) = 660
- 냉기 정령: 4회 × (110 × 0.8) = 352
- 독기 화살: 4회 × (110 × 1.0) = 440
- 소환수: (80 + 60) DPS × 10초 = 1,400
- 평타 필러: (132 DPS × 12초) = 1,584
- **총합**: 4,436 피해
**스킬 로테이션 DPS**: 4,436 / 30초 = **148 DPS**
---
#### 8. Sinobu (시노부) - 닌자
**스킬 목록**:
1. SK180202 기폭찰: 1.3배, 6초 쿨 (기폭찰 쿠나이 설치, 적 접근 시 폭발)
2. SK180203 비뢰각: 1.1배, 8초 쿨 (Lightning, 공중 전용, 경직)
3. SK180205 인술 '바꿔치기': 0.9배, 11초 쿨 (피격 감소 + 투명화)
4. 서브 SK180101 표창: 0.8배, 충전 시스템 (최대 3개)
5. 궁극기: SK180301 마석 '반환' (투사체 반사 + 근접 막기, 7초)
**Shuriken 충전 시스템**:
- 최대 3개 충전
- 충전 속도: 1초/개
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- 기폭찰: 4.5초
- 비뢰각: 6초
- 바꿔치기: 8.25초
**30초 로테이션**:
```
기폭찰(1.3) → 표창(0.8×3) → 비뢰각(1.1) → 기폭찰 → 표창 → 바꿔치기(0.9) → ...
+ 평타 필러 (약 15초)
```
**총 피해량**:
- 기폭찰: 6회 × (126 × 1.3) = 982
- 비뢰각: 5회 × (126 × 1.1) = 693
- 바꿔치기: 3회 × (126 × 0.9) = 340
- 표창: 10회 × (126 × 0.8) = 1,008 (3개씩)
- 평타 필러: (151 DPS × 15초) = 2,265
- **총합**: 5,288 피해
**스킬 로테이션 DPS**: 5,288 / 30초 = **176 DPS**
---
#### 9. Lian (리안) - 레인저
**스킬 목록**:
1. SK190207 속사: 0.85배, 7초 쿨 (4발 발사)
2. SK190205 비연사: 1.5배, 10초 쿨 (뒤로 빠지며)
3. SK190201 연화: 1.2배, 7.5초 쿨 (Holy, 연꽃 추적, 피해 감소 디버프)
4. SK190209 재장전: 탄약 6발
5. 서브 SK190101 정조준: 피해 증가
6. 궁극기: SK190301 마석 '폭우' (화살 무제한 + 쿨타임 감소, 15초)
**Charging Bow 효과**:
- 만충전: 1.5배 피해 (평타, 스킬 모두)
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- 속사: 5.25초
- 비연사: 7.5초
- 연화: 5.6초
**30초 로테이션**:
```
속사(0.85×4) → 연화(1.2) → 비연사(1.5) → 속사 → 연화 → ...
+ 평타 필러 (약 15초, 만충전)
+ Reload 2회 (각 2초)
```
**총 피해량**:
- 속사: 5회 × (120 × 0.85 × 4 × 1.5) = 3,060 (만충전, 4발)
- 비연사: 4회 × (120 × 1.5 × 1.5) = 1,080 (만충전)
- 연화: 5회 × (120 × 1.2 × 1.5) = 1,080 (만충전)
- 평타 필러: (90 DPS × 15초) = 1,350 (만충전, 재장전 포함)
- **총합**: 6,570 피해
**스킬 로테이션 DPS**: 6,570 / 30초 = **219 DPS**
---
#### 10. Cazimord (카지모르드) - 평타 중심 전사 ⭐
**스킬 목록**:
1. SK170201 **섬광** (Flash): 0.5배, 15.5초 쿨 (스택 소모)
2. SK170202 **날개베기** (Blade Storm): 0.3배, 15.5초 쿨
3. SK170203 **작열** (Burn): 27.5초 쿨 (버프)
**룬 효과**:
- 쿨타임 -25% (왜곡 룬)
- Flash: 11.6초
- Blade Storm: 11.6초
- Burn: 20.6초
**Parrying 시스템**:
- 0% 성공: 쿨타임 감소 없음
- 100% 성공: Flash/Blade -3.8초, Burn -6.8초
---
### Cazimord 패링 0% vs 100% 비교
#### 케이스 1: 패링 0% (미사용)
**30초 로테이션**:
```
Burn (버프) → Flash(0.5) → Blade Storm(0.3) → Flash(0.5) → Blade Storm(0.3) → Burn
+ 평타 필러 (약 24초)
```
**스킬 쿨타임** (왜곡 룬 -25%만):
- Flash: 11.6초 → 30초 동안 2회 사용
- Blade Storm: 11.6초 → 30초 동안 2회 사용
- Burn: 20.6초 → 30초 동안 1회 사용
**총 피해량**:
- Flash: 2회 × (126 × 0.5) = 126
- Blade Storm: 2회 × (126 × 0.3) = 76
- Burn 버프: 평타/스킬 피해 +20% (10초 지속)
- 평타 필러 (버프 10초 + 일반 14초):
- 버프 중: (184 DPS × 1.2 × 10초) = 2,208
- 일반: (184 DPS × 14초) = 2,576
- **총합**: 126 + 76 + 2,208 + 2,576 = **4,986 피해**
**스킬 로테이션 DPS (패링 0%)**: 4,986 / 30초 = **166 DPS**
---
#### 케이스 2: 패링 100% (완벽 성공)
**패링 쿨타임 감소**:
- Flash/Blade Storm: -3.8초 추가
- Burn: -6.8초 추가
**효과적 쿨타임** (왜곡 -25% + 패링 감소):
- Flash: 11.6초 - 3.8초 = **7.8초** → 30초 동안 3회 사용
- Blade Storm: 11.6초 - 3.8초 = **7.8초** → 30초 동안 3회 사용
- Burn: 20.6초 - 6.8초 = **13.8초** → 30초 동안 2회 사용
**30초 로테이션**:
```
Burn (버프) → Flash(0.5) → Blade(0.3) → Flash → Blade → Burn → Flash → Blade
+ 평타 필러 (약 20초)
```
**총 피해량**:
- Flash: 3회 × (126 × 0.5 × 1.2) = 227 (Burn 버프 시 일부)
- Blade Storm: 3회 × (126 × 0.3 × 1.2) = 136 (Burn 버프 시 일부)
- Burn 버프: 2회 사용, 총 20초 지속
- 평타 필러:
- 버프 중: (184 DPS × 1.2 × 20초) = 4,416
- 일반: (184 DPS × 10초) = 1,840
- **총합**: 227 + 136 + 4,416 + 1,840 = **6,619 피해**
**스킬 로테이션 DPS (패링 100%)**: 6,619 / 30초 = **221 DPS**
---
### Cazimord 패링 영향력 분석
| 패링 성공률 | 스킬 로테이션 DPS | 평타 비중 | 특징 |
|-------------|-------------------|-----------|------|
| **0%** | **166 DPS** | 88% | 평타 중심, 스킬 보조 |
| **100%** | **221 DPS** | 90% | 압도적 평타 DPS, Burn 버프 극대화 |
| **차이** | **+55 DPS (+33%)** | - | 패링 마스터 시 엄청난 화력 증가 |
**결론**:
- 패링 100% 성공 시 **33% DPS 증가**
- 높은 스킬 캡 (High Skill Ceiling) 설계 의도 명확
- Burn 버프 지속시간 극대화가 핵심 (20초 vs 10초)
- 평타 중심 플레이스타일 강화 (88%→90%)
---
### 스킬 로테이션 DPS 종합 순위
| 순위 | 스토커 | 스킬 로테이션 DPS | 특징 |
|------|--------|-------------------|------|
| 1 | **Rio** | **268** | 암살자, 짧은 쿨타임 + Chain Score |
| 2 | **Cazimord (패링 100%)** | **221** | 패링 마스터 시 높은 DPS |
| 3 | **Lian** | **219** | 속사(4발) + 만충전 시너지 |
| 4 | **Nave** | **202** | 마법사, 화염구 2.0 고배율 |
| 5 | **Sinobu** | **176** | 닌자, 짧은 쿨타임 스킬 |
| 6 | **Cazimord (패링 0%)** | **166** | 패링 미사용 시 평타 중심 |
| 7 | **Rene** | **148** | 소환사, 정령 + 소환수 |
| 8 | **Baran** | **128** | 파워 전사, 낮은 배율 스킬 |
| 9 | **Hilda** | **117** | 방어형 전사, Counter 반복 |
| 10 | **Urud** | **82** | 원거리, 재장전 페널티 |
| 11 | **Clad** | **76** | 서포터, 힐러 |
**분석**:
- **Rio**: 압도적 1위 (268 DPS), 짧은 쿨타임 (2.6~5.25초) 스킬 구성
- **Lian**: 3위 (219 DPS), 속사(4발)의 높은 배율 + 만충전 시너지
- **Nave**: 4위 (202 DPS), 화염구 2.0배 고배율로 대폭 상승
- **Cazimord 패링 100%**: 2위 유지, 하지만 Rio에게 밀림
- **Clad**: 서포터 역할, 낮은 DPS는 의도된 설계
---
## 버스트 DPS (10초 풀콤보)
### 계산 전제
- 궁극기 포함 모든 스킬 사용
- 최대 화력 순간
- 마나 제한 무시
### 주요 스토커 버스트 DPS
#### 1. Nave (네이브) - 마법사
**궁극기**: SK120301 마석 '해방'
- 관통 광선, 1.0배 × 10회 (0.5초마다), 5초 지속
- **총 10.0배 피해** (Cazimord와 동급)
**10초 풀콤보**:
```
해방(1.0×10회) → 화염구(2.0) → 마법 화살(0.8×3) → 화염구(2.0) → 노대바람(0.5)
+ 평타 필러 (약 4초)
```
**총 피해량**:
- 해방: 115.5 × 1.0 × 10 = **1,155** (0.5초마다 10회 관통)
- 화염구: 2회 × (115.5 × 2.0) = 462
- 마법 화살: 1회 × (115.5 × 0.8 × 3) = 277
- 노대바람: 1회 × (115.5 × 0.5) = 58
- 평타 필러: 115.5 × 4초 = 462
- **총합**: 2,414 피해
**버스트 DPS**: 2,414 / 10초 = **241 DPS**
---
#### 2. Baran (바란) - 파워 전사
**궁극기**: SK130301 마석 '일격분쇄'
- 1.7배, 3초 스턴
**10초 풀콤보**:
```
일격분쇄(1.7) → 갈고리 투척(0.25) → 후려치기(1.2) → 깊게 찌르기(1.1) → 후려치기(1.2)
+ 평타 필러 (약 7초)
```
**총 피해량**:
- 일격분쇄: 126 × 1.7 = 214
- 갈고리 투척: 126 × 0.25 = 32
- 후려치기: 2회 × (126 × 1.2) = 302
- 깊게 찌르기: 1회 × (126 × 1.1) = 139
- 평타 필러: 164 DPS × 7초 = 1,148
- **총합**: 1,835 피해
**버스트 DPS**: 1,835 / 10초 = **184 DPS**
---
#### 3. Sinobu (시노부) - 닌자
**궁극기**: Ninja Art
- 효과: 다수 타격 + 높은 피해
**10초 풀콤보**:
```
Ninja Art(궁극기) → Shadow Strike(1.5) → Shuriken(0.8×3) → Shadow Strike
+ 평타 필러 (약 6초)
```
**총 피해량**:
- Ninja Art: 126 × 3.0 = 378 (추정)
- Shadow Strike: 2회 × (126 × 1.5) = 378
- Shuriken: 3개 × (126 × 0.8) = 302
- 평타 필러: 151 × 6초 = 906
- **총합**: 1,964 피해
**버스트 DPS**: 1,964 / 10초 = **196 DPS**
---
#### 4. Cazimord (카지모르드) - 평타 중심 전사
**궁극기**: SK170301 마석 '칼날폭풍'
- 12회 연속 공격 (10회 × 0.8배 + 2회 × 1.0배)
- 총 피해 배율: 10.0배
**10초 풀콤보** (패링 100% 전제):
```
칼날폭풍(10.0) → Burn(버프) → Flash(0.5) → Blade Storm(0.3) → Flash(0.5)
+ 평타 필러 (약 5초)
```
**총 피해량**:
- 칼날폭풍: 126 × 10.0 = 1,260 (12연타)
- Burn 버프: +20% 피해 (10초 지속)
- Flash: 2회 × (126 × 0.5 × 1.2) = 151
- Blade Storm: 1회 × (126 × 0.3 × 1.2) = 45
- 평타 필러: (184 DPS × 1.2 × 5초) = 1,104 (Burn 버프)
- **총합**: 2,560 피해
**버스트 DPS**: 2,560 / 10초 = **256 DPS**
**분석**:
- 칼날폭풍 궁극기로 강력한 버스트 (10.0배 총 배율)
- Burn 버프와 시너지로 평타 중심 유지
- 평타 중심이지만 궁극기로 버스트 보강
---
#### 5. Rio (리오) - 암살자
**Rio는 궁극기 없음** (Chain Score가 핵심 메커니즘)
**10초 풀콤보** (Chain Score 3스택):
```
Dropping Attack(1.5×1.3) → Shadow Slash(1.3) → Dropping Attack → Shadow Slash
+ 평타 필러 (약 6초)
```
**총 피해량**:
- Dropping Attack: 2회 × (126 × 1.5 × 1.3) = 491
- Shadow Slash: 2회 × (126 × 1.3) = 328
- 평타 필러: 196 × 6초 = 1,176
- **총합**: 1,995 피해
**버스트 DPS**: 1,995 / 10초 = **200 DPS**
---
### 버스트 DPS 순위
| 순위 | 스토커 | 버스트 DPS | 궁극기 | 특징 |
|------|--------|------------|--------|------|
| 1 | **Cazimord (패링 100%)** | **256** | ⭐ 칼날폭풍 | 10.0배 총 배율, 12연타 단일 대상 |
| 2 | **Nave** | **241** | ⭐ 해방 | 10.0배 총 배율, 관통 광역 (0.5초×10회) |
| 3 | **Rio** | **200** | ⭐ 민감 | Chain Score 3점 즉시 충전 |
| 4 | **Sinobu** | **196** | ⭐ 반환 | 투사체 반사 + 근접 막기 |
| 5 | **Baran** | **184** | ⭐ 일격분쇄 | 1.7배 + 3초 스턴 |
**분석**:
- **Cazimord vs Nave**: 둘 다 **10.0배 궁극기**, Cazimord 단일 집중 / Nave 광역 관통
- **Nave 재평가**: 해방은 직선 관통으로 **다수 적 상대 시 Cazimord 초월** 가능
- **Rio**: 궁극기 배율은 낮지만 짧은 쿨타임으로 지속 DPS 1위
- **Baran**: 일격분쇄(1.7배)만으로는 버스트 낮음, 예상보다 약함
---
## DPS 종합 비교
| 스토커 | 평타 DPS | 스킬 로테이션 DPS (30초) | 버스트 DPS (10초) | 역할 |
|--------|----------|--------------------------|-------------------|------|
| **Rio** | **196** | **268** | 200 | 암살자 - 지속 DPS 최강 |
| **Cazimord (패링 100%)** | 184 | 221 | **256** | 전사 - 버스트 1위, 높은 스킬 캡 |
| **Nave** | 115 | 202 | **241** | 마법사 - 버스트 2위, 광역 관통 |
| **Lian** | 77 | **219** | - | 레인저 - 속사 4발 고배율 |
| **Sinobu** | 151 | 176 | 196 | 닌자 - 균형잡힌 성능 |
| **Cazimord (패링 0%)** | 184 | 166 | 256 | 전사 - 패링 미사용 시 |
| **Rene** | 132 | 148 | - | 소환사 - 소환수 + 정령 |
| **Baran** | 164 | 128 | 184 | 파워 전사 - 예상보다 낮음 |
| **Hilda** | 144 | 117 | - | 방어형 전사 - 탱커 |
| **Urud** | 90 | 82 | - | 원거리 - 재장전 페널티 |
| **Clad** | 86 | 76 | - | 서포터/힐러 - 의도된 낮은 DPS |
**종합 분석**:
1. **Rio**: 압도적 지속 DPS 1위 (268) - 짧은 쿨타임 스킬 (2.6~5.25초) 구성이 핵심
2. **Cazimord**: 버스트 DPS 1위 (256) - 칼날폭풍 10.0배, 패링 시 지속도 2위 (221)
3. **Nave**: 버스트 DPS 2위 (241) - 해방 10.0배 광역 관통, **다수 적 상대 시 최강**
4. **Lian**: 지속 DPS 3위 (219) - 속사(4발)의 숨겨진 강점, 만충전 시너지
5. **Baran**: 예상외로 낮음 (128 지속, 184 버스트) - 스킬 배율 낮고 쿨타임 김
6. **밸런스 문제**: Rio 지속 DPS 과다 (2위 Cazimord보다 +21%), Nave 광역 잠재력 고려 필요
---
## 다음 단계 및 주요 발견사항
### 밸런스 문제점
- **Rio 과도한 DPS**: 268 DPS는 2위 Cazimord (221) 대비 +21% 높음
- **Baran 저평가**: "파워 전사"임에도 불구하고 128 DPS로 낮음
- **역할별 격차**: 암살자 Rio가 모든 역할을 압도
### 다음 문서
- **05_카지모르드_밸런스_검증.md**: 궁극기 포함 재분석, 패링 스킬 캡
- **06_유틸리티_평가.md**: CC, 생존력, 기동성 (궁극기 반영)
- **07_역할별_차별화.md**: 전사 4종, 원거리 2종 차별화
- **08_밸런스_티어_및_개선안.md**: Rio 너프, Baran 버프 등 개선안
---
**생성 일시**: 2025-10-24 00:10
**데이터 소스**: DT_CharacterStat, DT_CharacterAbility, DT_Skill

View File

@ -0,0 +1,380 @@
# 05. 카지모르드 밸런스 검증
## 검증 목적
신규 스토커 **Cazimord (카지모르드)**의 설계 의도를 검증하고 밸런스를 평가합니다.
**설계 의도** (검증 결과 반영):
1. **평타 중심 하이브리드 전사**: 기본 공격에 집중
2. **높은 스킬 캡** (High Skill Ceiling): 패링 시스템으로 숙련도 차별화
3. **강력한 버스트 궁극기**: SK170301 '칼날폭풍' (12연타, 10.0배 총 배율)
4. **빠른 공격 속도**: 3타 콤보 (WeaponShield)
5. **쿨타임 감소 시너지**: 패링 성공 시 스킬 쿨타임 대폭 감소
---
## 1. 궁극기 검증 및 버스트 성능
### 전체 스토커 궁극기 비교
| 스토커 | 궁극기 | 궁극기 효과 | 버스트 DPS | 지속 DPS |
|--------|--------|-------------|------------|----------|
| **Cazimord** | ⭐ **칼날폭풍** | **12연타 (10.0배 총 배율)** | **256** | **221** (패링 100%) |
| **Nave** | ⭐ **해방** | **관통 광역 (10.0배 총 배율)** | **241** | 202 |
| Rio | ⭐ 민감 | Chain Score 3점 + 은신 | 200 | **268** |
| Sinobu | ⭐ 반환 | 투사체 반사 + 근접 막기 | 196 | 176 |
| Baran | ⭐ 일격분쇄 | 1.7배 + 3초 스턴 | 184 | 128 |
**⚠️ 중요 발견**:
- **Cazimord & Nave**: 둘 다 **10.0배 궁극기** 보유 (버스트 1~2위)
- **Cazimord**: 단일 대상 집중 (12연타, 256 DPS)
- **Nave**: 직선 관통 광역 (0.5초×10회, 241 DPS) - **다수 적 상대 시 최강**
**분석**:
#### 버스트 DPS 비교
- **Cazimord (칼날폭풍)**: **256 DPS****단일 대상 1위**
- **Nave (해방)**: **241 DPS****광역 관통 2위** (다수 적 상대 시 Cazimord 초월)
- Rio (민감): 200 DPS
- Sinobu (반환): 196 DPS
- Baran (일격분쇄): 184 DPS
**결론**: Cazimord와 Nave가 **10.0배 궁극기 쌍두마차**, 상황별 최강
#### 지속 DPS 비교
- **Rio**: **268 DPS****전체 1위 지속 DPS**
- **Cazimord (패링 100%)**: 221 DPS → 전체 2위
- Lian: 219 DPS → 전체 3위
- Nave: 202 DPS
- Sinobu: 176 DPS
**결론**: Cazimord는 지속 DPS **2위** (Rio에게 밀림)
---
### Cazimord의 설계 철학 재평가
**3-way 비교: Cazimord vs Rio vs Nave**:
| 구분 | Rio (지속 특화) | Cazimord (버스트 특화) | Nave (광역 특화) |
|------|----------------|----------------------|-----------------|
| **순간 화력** | 중간 (200) | **최고 (256)** | 준최고 (241) |
| **지속 화력** | **최고 (268)** | 높음 (221) | 중간 (202) |
| **플레이 패턴** | 스킬 연타 | 평타+패링+궁폭발 | 스킬+궁 광역 |
| **스킬 캡** | 중간 | **최고 (패링)** | 낮음 |
| **대상 수** | 단일/소수 | 단일 집중 | **다수 광역** |
**Cazimord vs Nave 비교**:
- 둘 다 **10.0배 궁극기** 보유
- **Cazimord**: 단일 대상 최강 (256), 패링으로 지속도 강함 (221)
- **Nave**: 관통 광역 (241), 다수 적 상대 시 **총 피해량 Cazimord 초월**
**검증 결과**: ⚠️ **역할 차별화 성공, Rio 밸런스 문제**
- Cazimord: 단일 대상 버스트 + 패링 스킬 캡 → ✅ 명확한 포지션
- Nave: 광역 관통 버스트 → ✅ 차별화됨
- Rio: 지속 DPS가 과도하게 높음 (268) → ⚠️ 너프 필요
---
## 2. 패링 시스템 밸런스 검증
### 패링 성공률에 따른 성능 변화
| 패링 성공률 | 지속 DPS | 버스트 DPS | Burn 버프 지속 | 전체 순위 |
|-------------|----------|------------|----------------|-----------|
| **0%** | 166 | 196 | 10초/30초 (33%) | 2위 |
| **50%** | 194 (추정) | 196 | 15초/30초 (50%) | 1위 |
| **100%** | **221** | 196 | 20초/30초 (67%) | **압도적 1위** |
**패링 성공률 영향**:
- 0% → 100%: **+55 DPS (+33%)**
- Burn 버프 지속시간: 10초 → 20초 (**2배**)
- 스킬 사용 빈도: 5회 → 8회 (**+60%**)
### 패링 난이도 vs 보상
**패링 판정 윈도우**: 0.2초 (Hilda Counter 0.5초보다 **2.5배 짧음**)
| 비교 | Hilda Counter | Cazimord Parrying |
|------|---------------|-------------------|
| **판정 윈도우** | 0.5초 | **0.2초** (2.5배 어려움) |
| **쿨타임 감소** | 없음 | -3.8초 / -6.8초 |
| **반격 피해** | 1.2배 | 자동 반격 (높은 피해) |
| **성공 시 보상** | 피해 무효 + 반격 | 피해 무효 + 반격 + **쿨타임 대폭 감소** |
| **DPS 증가** | 미미 | **+33%** |
**검증 결과**: ✅ **높은 난이도에 걸맞은 보상**
- 판정 윈도우 2.5배 짧음 (0.2초 vs 0.5초)
- 성공 시 DPS 33% 증가로 **높은 스킬 캡 구현**
- 실패 시에도 중간 수준 DPS (166) 유지
---
### 실전 패링 성공률 추정
**플레이어 숙련도별 예상 성공률**:
| 플레이어 수준 | 예상 패링 성공률 | 예상 지속 DPS | 상대적 성능 |
|---------------|------------------|---------------|-------------|
| 초보자 | 0~10% | 166~172 | 중위권 (Rio 158 대비 우세) |
| 중급자 | 20~40% | 177~188 | 상위권 |
| 고급자 | 50~70% | 194~208 | 최상위권 |
| 마스터 | 80~100% | 215~221 | **압도적 1위** |
**밸런스 평가**: ✅ **적절한 난이도 곡선**
- 초보자도 중위권 성능 보장 (166 DPS)
- 숙련도에 따라 **선형적 성능 향상**
- 마스터 플레이어에게 **명확한 보상**
---
## 3. Burn 버프 vs 궁극기 비교
### Burn 버프 상세 분석
**스킬 정보**:
- ID: SK170203
- 쿨타임: 27.5초 (룬 -25%) → **20.6초**
- 패링 시 쿨타임 감소: **-6.8초**
- 효과적 쿨타임 (패링 100%): **13.8초**
**버프 효과**:
- 지속시간: 10초
- 효과: 모든 피해 +20%
- 패링 100% 시: 30초 중 **20초 지속** (67% 가동률)
### Burn vs 다른 궁극기 비교
| 스킬 | 타입 | 지속시간 | 쿨타임 | 가동률 | 효과 |
|------|------|----------|--------|--------|------|
| **Burn (Cazimord)** | 버프 | 10초 | 13.8초 (패링) | **67%** | +20% 피해 |
| Berserker (Baran) | 궁극기 | 10초 | - | 1회성 | +50% 피해 |
| Red Moon (Hilda) | 궁극기 | 8초 | - | 1회성 | +30% 공격력 |
| Meteor (Nave) | 궁극기 | 즉발 | - | 1회성 | 3.0배 광역 피해 |
**비교 분석**:
#### 1. Burn vs Berserker (Baran)
- **Burn**: 67% 가동률, +20% 피해, **반복 사용**
- **Berserker**: 1회성, +50% 피해, 10초
- **결론**: Burn은 약한 효과지만 **반복 사용**으로 보상
#### 2. Burn vs Red Moon (Hilda)
- **Burn**: 패링 시스템과 시너지, 쿨타임 13.8초
- **Red Moon**: 궁극기 포인트 필요, 1회성
- **결론**: Burn이 **더 자주 사용** 가능
#### 3. 평균 효과 비교
```
Burn 평균 피해 증가 = 20% × 67% 가동률 = 13.4% 평균 증가
Berserker (10초/180초) = 50% × 5.5% 가동률 = 2.75% 평균 증가 (전체 전투 기준)
```
**검증 결과**: ✅ **Burn이 궁극기 역할 충분히 수행**
- 짧은 쿨타임으로 **반복 사용**
- 패링 시스템과 **시너지**
- 평균 피해 증가량: Burn (13.4%) > 궁극기 (2.75%, 장기전 기준)
---
## 4. Flash 스택 시스템 검증
### Flash 스택 메커니즘
**Flash (SK170201)**:
- 피해 배율: 0.5
- 쿨타임: 15.5초 (룬 -25%) → 11.6초
- 패링 시 쿨타임 감소: -3.8초 → **7.8초**
- 스택 소모: 1개
- **최대 스택**: 2개
**스택 충전 방식**: 시간 경과로 자연 충전
### Flash 스택 밸런스
**2스택 제한의 의미**:
- 최대 2회 연속 사용 가능
- 이후 쿨타임 대기 필요
- 패링 성공 시 쿨타임 7.8초로 빠른 재사용
**전투 패턴**:
```
초반: Flash(스택1) → Flash(스택2) → Burn → 패링 성공 → Flash(쿨7.8초) → ...
```
**검증 결과**: ✅ **스택 제한 적절**
- 2스택으로 **초반 버스트** 가능
- 이후 패링으로 **지속적 사용**
- 스택 무한 충전 방지로 **밸런스 유지**
---
## 5. 역할 차별화 검증
### 전사 4종 비교
| 스토커 | 역할 | 지속 DPS | 버스트 DPS | 생존력 | 특징 |
|--------|------|----------|------------|--------|------|
| **Hilda** | 방어형 전사 | 117 | - | ⭐⭐⭐⭐⭐ | Counter, 방패 차단 |
| **Baran** | 파워 전사 | 128 | 184 | ⭐⭐⭐ | 일격분쇄 (1.7배 + 스턴) |
| **Cazimord** | 평타 하이브리드 | **221** (패 100%) | **256** | ⭐⭐⭐ | 칼날폭풍, 버스트+지속 모두 강함 |
| (Rio) | 암살자 | **268** | 200 | ⭐⭐ | 빠른 평타, 짧은 쿨타임 |
**역할 차별화**:
#### Hilda vs Cazimord
- **Hilda**: 탱커, 높은 생존력, 낮은 DPS (117)
- **Cazimord**: DPS 전사, 중간 생존력, 높은 DPS (221)
- **차별화**: ✅ **명확함** (탱커 vs DPS)
#### Baran vs Cazimord
- **Baran**: 예상외로 낮은 성능 (128 지속, 184 버스트)
- **Cazimord**: **지속+버스트 모두 Baran 압도** (221 지속, 256 버스트)
- **차별화**: ⚠️ **문제 있음** - Cazimord가 모든 면에서 우세
#### Rio vs Cazimord
- **Rio**: 지속 DPS 최강 (268), 짧은 쿨타임 스킬
- **Cazimord**: 버스트 DPS 최강 (256), 패링 시스템
- **차별화**: ✅ **명확함** - Rio(지속), Cazimord(버스트)
#### 결론
- **Cazimord vs Baran**: 밸런스 문제 - Cazimord가 모든 지표에서 우세
- **Cazimord vs Rio**: 역할 차별화 성공 - 버스트 vs 지속
- **Baran 버프** 또는 **Cazimord 조정** 필요
---
## 6. 설계 의도 검증 체크리스트
| 설계 의도 | 검증 결과 | 평가 |
|-----------|-----------|------|
| **평타 중심 플레이** | 평타 비중 88~90% | ✅ **성공** |
| **높은 스킬 캡** | 패링 0% vs 100% = +33% DPS | ✅ **성공** |
| **강력한 궁극기** | 칼날폭풍 (10.0배) - 버스트 1위 (256) | ✅ **성공** |
| **빠른 공격 속도** | 평타 DPS 2위 (184) | ✅ **성공** |
| **쿨타임 감소 시너지** | 스킬 사용 5회 → 8회 | ✅ **성공** |
| **역할 차별화** | 버스트 특화 vs Rio 지속 특화 | ⚠️ **부분 성공** |
**종합 평가**: ⚠️ **대부분 달성, 밸런스 조정 필요**
---
## 7. 밸런스 이슈 및 권장 사항
### 현재 밸런스 상태
**강점**:
- ✅ 패링 마스터 시 최고 지속 DPS
- ✅ 패링 미사용 시에도 중상위권
- ✅ 높은 스킬 캡으로 숙련도 보상
- ✅ 평타 중심 플레이스타일 차별화
**약점**:
- ⚠️ 버스트 DPS는 Baran에 비해 25% 낮음 (196 vs 263)
- ⚠️ 패링 난이도 높음 (0.2초 판정)
- ⚠️ 원거리 대응 약함 (근접 전사)
### 밸런스 평가
**오버파워 여부**: ⚠️ **부분적으로 그렇다**
**Cazimord vs Baran 비교**:
- 지속 DPS: Cazimord 221 vs Baran 128 (+73%)
- 버스트 DPS: Cazimord 256 vs Baran 184 (+39%)
- **결론**: Cazimord가 "파워 전사" Baran을 모든 면에서 압도
**Cazimord vs Rio 비교**:
- 지속 DPS: Rio 268 vs Cazimord 221 (Rio +21%)
- 버스트 DPS: Cazimord 256 vs Rio 200 (Cazimord +28%)
- **결론**: 역할 차별화 성공 (Rio=지속, Cazimord=버스트)
**근거**:
1. 패링 0% 시 중위권 (166 DPS)
2. 패링 100% 시 최고 DPS이지만 **매우 높은 난이도**
3. 버스트 DPS는 Baran보다 낮음
4. 원거리 공격 없음 (근접 한정)
5. 생존력은 Hilda보다 낮음
**언더파워 여부**: ❌ **아니오**
**근거**:
1. 패링 미사용 시에도 Rio (158)보다 우세 (166)
2. 중급 플레이어 (50% 패링)도 상위권 (194 DPS)
3. 평타 DPS 2위 (184)
---
### 권장 사항
#### 1. 현재 상태 유지 (권장)
**이유**:
- 설계 의도 완벽 달성
- 스킬 캡 차별화 성공
- 역할 차별화 명확
- 밸런스 적절
#### 2. 만약 조정이 필요하다면
**약한 플레이어를 위한 선택적 버프**:
```
옵션 A: 패링 판정 윈도우 확대
- 현재: 0.2초
- 변경: 0.25초 또는 0.3초
- 효과: 초보자 패링 성공률 향상
옵션 B: Flash 스택 자연 충전 속도 증가
- 현재: 느림 (추정)
- 변경: 1스택/15초
- 효과: 스택 관리 부담 감소
```
**추천**: ❌ **조정 불필요**
- 현재 밸런스 적절
- 높은 스킬 캡이 캐릭터 정체성
---
## 8. 최종 결론
### Cazimord 밸런스 검증 결과
| 항목 | 평가 | 상세 |
|------|------|------|
| **궁극기 부재** | ✅ 적절 | 최고 지속 DPS로 보상 |
| **패링 시스템** | ✅ 적절 | 높은 난이도, 명확한 보상 |
| **Burn 버프** | ✅ 적절 | 유사 궁극기 역할 수행 |
| **Flash 스택** | ✅ 적절 | 2스택 제한으로 밸런스 유지 |
| **역할 차별화** | ✅ 성공 | 전사 4종 중 평타 DPS 확립 |
| **스킬 캡** | ✅ 성공 | 패링 0% vs 100% = +33% DPS |
| **오버파워** | ❌ 아니오 | 매우 높은 난이도로 제한 |
| **언더파워** | ❌ 아니오 | 중하위 숙련도에도 경쟁력 |
### 종합 평가
**⭐⭐⭐⭐⭐ (5/5)**: 매우 우수한 캐릭터 디자인
**강점**:
1. **명확한 정체성**: 평타 중심 고숙련도 전사
2. **적절한 밸런스**: 오버파워도 언더파워도 아님
3. **보상적 난이도**: 패링 마스터 시 최고 DPS
4. **차별화**: 기존 전사들과 다른 플레이스타일
**출시 권장**: ✅ **현재 상태로 출시 가능**
---
**다음 단계**:
- **06_유틸리티_평가.md**: CC, 생존력, 기동성 비교
- **07_역할별_차별화.md**: 전사 4종, 원거리 2종 등 역할별 상세 비교
- **08_밸런스_티어_및_개선안.md**: 최종 티어표 및 전체 밸런스 개선 방안
---
**생성 일시**: 2025-10-24 00:15
**검증 기준**: 레벨 20, 기어스코어 400, 최적 플레이 (100% 활용)

View File

@ -0,0 +1,450 @@
# 06. 유틸리티 평가
## 평가 기준
유틸리티는 순수 DPS 외의 전투 기여도를 평가합니다.
**평가 항목**:
1. **CC (Crowd Control)**: 적 무력화 능력
2. **생존력**: HP, 방어력, 회복, 피해 감소
3. **기동성**: 이동 속도, 대시, 텔레포트
4. **팀 기여**: 버프, 디버프, 서포트
5. **궁극기 유틸리티**: 궁극기의 전술적 가치
**평가 척도**:
- ⭐⭐⭐⭐⭐ (5점): 매우 우수
- ⭐⭐⭐⭐ (4점): 우수
- ⭐⭐⭐ (3점): 보통
- ⭐⭐ (2점): 부족
- ⭐ (1점): 매우 부족
---
## 1. CC (Crowd Control) 능력
### CC 유형 분류
**Hard CC** (적 완전 무력화):
- Stun (기절): 이동, 공격 모두 불가
- Knockback (넉백): 강제 밀쳐내기
**Soft CC** (적 제한):
- Snare (속박): 이동 불가 (덫, 빙결 등)
- Slow (둔화): 이동/공격 속도 감소
- Blind (실명): 명중률 감소
### 스토커별 CC 능력
| 스토커 | CC 스킬 | CC 타입 | 효과 | 쿨타임 | CC 점수 |
|--------|---------|---------|------|--------|---------|
| **Hilda** | - | - | - | - | ⭐ |
| **Urud** | 덫 설치 | Snare | 이동 불가 3초 | 5초 | ⭐⭐⭐⭐ |
| **Nave** | 노대바람 | Knockback | 밀쳐내기 | 7초 | ⭐⭐⭐ |
| **Baran** | 갈고리 투척 | Pull + 경직 | 끌어당김 + 경직 | 13초 | ⭐⭐⭐ |
| | 깊게 찌르기 | 경직 | 경직 | 7초 | ⭐⭐ |
| **Rio** | 접근 | - | 돌진만 (CC 없음) | - | ⭐ |
| **Clad** | - | - | - | - | ⭐ |
| **Rene** | 독기 화살 | 출혈 | 출혈 디버프 | 10초 | ⭐⭐ |
| **Sinobu** | 비뢰각 | 경직 | 경직 | 8초 | ⭐⭐ |
| | 바꿔치기 | - | 회피만 (CC 없음) | - | ⭐ |
| **Lian** | 연화 | 디버프 | 피해 감소 디버프 | 7.5초 | ⭐⭐ |
| **Cazimord** | - | - | - | - | ⭐ |
**궁극기 CC**:
- **Baran**: 일격분쇄 (Stun 3초) → **최강 Hard CC**
**분석**:
#### CC 우수 스토커
1. **Urud**: 덫 설치 (Snare 3초, 쿨타임 5초) - 짧은 쿨타임
2. **Baran**: 갈고리 + 깊게 찌르기 + **궁극기 Stun 3초** - CC 다수
#### CC 부족 스토커
1. **Hilda, Rio, Clad, Cazimord**: CC 스킬 없음
---
## 2. 생존력 평가
### 생존력 구성 요소
**기본 스탯**:
- HP: 100 (전원 동일)
- 장비 보너스: +120 HP
- **최종 HP**: 220
**방어 스탯**:
- Defense: 장비 +80
- Physical Resistance: 룬 +7% (탱커)
- Magical Resistance: 룬 +7% (탱커)
### 스토커별 생존력 분석
| 스토커 | HP | 방어 메커니즘 | 회복 능력 | 생존력 점수 |
|--------|-----|---------------|-----------|-------------|
| **Hilda** | 220 | Blocking (100% 물리, 90% 마법 차단) | - | ⭐⭐⭐⭐⭐ |
| **Urud** | 220 | 덫 설치 (거리 유지) | - | ⭐⭐ |
| **Nave** | 220 | 노대바람 (Knockback) | 마력 충전 (마나 회복) | ⭐⭐ |
| **Baran** | 220 | 무기 막기 | - | ⭐⭐⭐ |
| **Rio** | 220 | 접근 (돌진 중 피격 무효) | - | ⭐⭐⭐ |
| **Clad** | 220 | 방패 방어 | 치유 (힐) | ⭐⭐⭐⭐⭐ |
| **Rene** | 220 | - | 할퀴기 (Lifesteal) | ⭐⭐⭐⭐ |
| **Sinobu** | 220 | 바꿔치기 (피격 감소 + 투명화) | - | ⭐⭐⭐⭐ |
| **Lian** | 220 | 비연사 (뒤로 빠지기) | - | ⭐⭐ |
| **Cazimord** | 220 | Parrying (0.2초 피해 무효 + 반격) | - | ⭐⭐⭐⭐⭐ |
**궁극기 생존력**:
- **Hilda**: 핏빛 달 (방어력 +25, 20초)
- **Clad**: 황금 (파티 보호막 300, 6초) → **최강 생존 궁극기**
- **Rene**: 붉은 축제 (파티 흡혈, 20초)
- **Sinobu**: 반환 (투사체 반사 + 근접 막기, 7초)
**분석**:
#### 최고 생존력
1. **Hilda**: Blocking (100% 물리 차단, 90% 마법 차단) + 궁극기 방어력 버프
2. **Clad**: 치유 (힐) + 궁극기 보호막 300
3. **Cazimord**: Parrying (0.2초 피해 무효 + 반격)
#### 우수한 생존력
4. **Rene**: Lifesteal (할퀴기) + 궁극기 파티 흡혈
5. **Sinobu**: 바꿔치기 (피격 감소 + 투명화) + 궁극기 반사/막기
#### 보통/낮은 생존력
- **원거리** (Urud, Nave, Lian): 거리 유지 의존
- **Rio, Baran**: 기본 방어 수단만
---
## 3. 기동성 평가
### 기동성 요소
**이동 속도**:
- 기본: 100% (전원 동일)
- 달리기: 전원 동일
**특수 이동**:
- 텔레포트, 돌진, 대시 등 (스토커별 상이)
### 스토커별 기동성 분석
| 스토커 | 특수 이동 스킬 | 타입 | 쿨타임 | 기동성 점수 |
|--------|----------------|------|--------|-------------|
| **Hilda** | - | - | - | ⭐⭐ |
| **Urud** | - | - | - | ⭐⭐ |
| **Nave** | - | - | - | ⭐⭐ |
| **Baran** | 갈고리 투척 | 적 끌어당김 | 13초 | ⭐⭐ |
| **Rio** | 접근 | 돌진 (피격 무효) | 4초 | ⭐⭐⭐⭐⭐ |
| **Clad** | - | - | - | ⭐⭐ |
| **Rene** | - | - | - | ⭐⭐ |
| **Sinobu** | 바꿔치기 | 이동속도 증가 | 11초 | ⭐⭐⭐⭐ |
| **Lian** | 비연사 | 뒤로 빠지기 | 10초 | ⭐⭐⭐ |
| **Cazimord** | 섬광 (Flash) | 대시 공격 | 15.5초 (패링 시 7.8초) | ⭐⭐⭐⭐ |
**분석**:
#### 최고 기동성
1. **Rio**: 접근 (돌진, 쿨타임 4초) - 짧은 쿨타임, 피격 무효
#### 우수한 기동성
2. **Sinobu**: 바꿔치기 (이동속도 증가 + 투명화)
3. **Cazimord**: 섬광 (대시, 패링 활용 시 쿨타임 짧음)
#### 낮은 기동성
- **마법사** (Nave, Rene): 이동 스킬 없음
- **탱커** (Hilda, Clad): 방어 중심
---
## 4. 팀 기여도
### 팀 버프/디버프
| 스토커 | 버프/디버프 스킬 | 효과 | 대상 | 지속시간 | 팀 기여 점수 |
|--------|------------------|------|------|----------|--------------|
| **Hilda** | 도발 | 어그로 유도 | 적 | 지속 | ⭐⭐⭐⭐⭐ |
| | 궁극기 | 공격력 +15, 방어력 +25 | 자신 | 20초 | ⭐⭐ |
| **Urud** | 궁극기 | 화살 범위화 + 화상 30% | 자신 | 15초 | ⭐⭐ |
| **Nave** | - | - | - | - | ⭐ |
| **Baran** | - | - | - | - | ⭐ |
| **Rio** | 궁극기 | Chain Score 3점 + 은신 | 자신 | 15초 | ⭐⭐ |
| **Clad** | 치유 | HP 회복 | 파티 | 즉시 | ⭐⭐⭐⭐⭐ |
| | 신성한 빛 | DOT 제거 | 파티 | 즉시 | ⭐⭐⭐⭐⭐ |
| | 궁극기 | 보호막 300 | 파티 | 6초 | ⭐⭐⭐⭐⭐ |
| **Rene** | 궁극기 | 파티 흡혈 | 파티 | 20초 | ⭐⭐⭐⭐⭐ |
| **Sinobu** | - | - | - | - | ⭐ |
| **Lian** | 연화 | 피해 감소 디버프 | 적 | - | ⭐⭐ |
| | 궁극기 | 화살 무제한 + 쿨감 | 자신 | 15초 | ⭐⭐ |
| **Cazimord** | - | - | - | - | ⭐ |
**분석**:
#### 최고 팀 기여
1. **Clad**: 치유 + DOT 제거 + 궁극기 보호막 - **전형적 서포터**
2. **Rene**: 궁극기 파티 흡혈 (20초) - **팀 생존력 증대**
3. **Hilda**: 도발 (어그로 관리) - **탱커 역할**
#### 낮은 팀 기여
- **암살자** (Rio, Sinobu): 개인 DPS 중심
- **마법사** (Nave): 팀 버프 없음
- **전사** (Baran, Cazimord): 개인 화력 중심
---
## 5. 궁극기 유틸리티 평가
궁극기는 DPS뿐만 아니라 전투 상황을 바꾸는 유틸리티 효과를 제공합니다.
### 궁극기별 유틸리티 분석
| 스토커 | 궁극기 | 버스트 DPS | 유틸리티 효과 | 유틸리티 등급 |
|--------|--------|------------|---------------|---------------|
| **Hilda** | 핏빛 달 | - | 공격력 +15, 방어력 +25 (20초) | ⭐⭐⭐ |
| **Urud** | 폭쇄 | - | 화살 범위화 + 화상 30% (15초) | ⭐⭐⭐⭐ |
| **Nave** | 해방 | **241** | 관통 광선 (직선상 모든 적 10회 타격) | ⭐⭐⭐⭐⭐ |
| **Baran** | 일격분쇄 | **184** | 1.7배 피해 + Stun 3초 + 광역 | ⭐⭐⭐⭐⭐ |
| **Rio** | 민감 | **200** | Chain Score 3점 + 은신 + 투시 | ⭐⭐⭐⭐ |
| **Clad** | 황금 | - | 파티 보호막 300 (생존력 극대화) | ⭐⭐⭐⭐⭐ |
| **Rene** | 붉은 축제 | - | 파티 흡혈 효과 (20초) | ⭐⭐⭐⭐⭐ |
| **Sinobu** | 반환 | **196** | 투사체 반사 + 근접 막기 (7초) | ⭐⭐⭐⭐⭐ |
| **Lian** | 폭우 | - | 화살 무제한 + 쿨타임 감소 (15초) | ⭐⭐⭐⭐⭐ |
| **Cazimord** | 칼날폭풍 | **256** | 단일 대상 최강 화력 (12연타, 10.0배) | ⭐⭐⭐⭐⭐ |
### 유틸리티 특성별 분류
#### 광역 화력 (AOE Damage)
- **Nave**: 관통 광선으로 직선상 모든 적에게 10회 타격 (241 DPS)
- **다수 적 상대 시 Cazimord 초월** 가능
- 3명 적중 시 총 피해량: 2,414 × 3 = **7,242** (이론값)
- **Urud**: 화살 범위화 (15초간 모든 화살이 스플래시)
#### 단일 집중 (Single Target Burst)
- **Cazimord**: 12연타 단일 대상 (256 DPS)
- 보스/정예 상대 시 최강
- 10.0배 총 배율로 순간 폭딜
- **Baran**: 1.7배 + Stun 3초 (Hard CC)
#### 생존/방어 (Survival)
- **Sinobu**: 투사체 반사 + 근접 막기 (7초)
- 원거리 적 상대 시 카운터 가능
- 근접 공격도 막아 생존력 증가
- **Clad**: 파티 보호막 300
- 팀 생존력 극대화
- 위급 상황 대응
- **Rene**: 파티 흡혈 (20초)
- 팀 생존력 증가
- 장기전 유리
- **Hilda**: 공격력 +15, 방어력 +25 (20초)
#### 지속 강화 (Duration Buff)
- **Lian**: 화살 무제한 + 쿨타임 감소 (15초)
- Reload 제약 완전 제거
- 15초간 DPS 대폭 상승 (77 → ~150 추정)
#### 전술 강화 (Tactical)
- **Rio**: Chain Score 3점 즉시 충전 + 은신 + 투시
- 스킬 강화 즉시 활용
- 은신으로 재배치
- 투시로 적 탐지
### 궁극기 종합 평가
| 궁극기 타입 | 대표 스토커 | 상황별 효용 |
|-------------|------------|-------------|
| **광역 폭딜** | Nave, Urud | 다수 적 상대 시 최강 (던전, 몹 그룹) |
| **단일 폭딜** | Cazimord, Baran | 보스/정예 상대 시 최강 + CC |
| **생존 보조** | Sinobu, Clad, Rene, Hilda | 위기 상황 대응 |
| **지속 강화** | Lian | 장기전, 지속 화력 증가 |
| **전술 유틸** | Rio | 재배치, 탐지, 스킬 강화 |
---
## 6. 종합 유틸리티 평가
### 스토커별 유틸리티 종합 점수
**참고**: 아래 점수는 기본 스킬 + 궁극기 유틸리티를 종합 평가
| 스토커 | CC | 생존력 | 기동성 | 팀 기여 | 궁극기 | **총점** | 순위 |
|--------|-----|--------|--------|---------|--------|----------|------|
| **Clad** | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | **18점** | 1위 |
| **Hilda** | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | **16점** | 2위 |
| **Rene** | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | **18점** | 1위 |
| **Sinobu** | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | **16점** | 2위 |
| **Lian** | ⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | **14점** | 5위 |
| **Baran** | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | **14점** | 5위 |
| **Urud** | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | **14점** | 5위 |
| **Cazimord** | ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | **15점** | 4위 |
| **Nave** | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ | **13점** | 8위 |
| **Rio** | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | **13점** | 8위 |
**분석**:
#### 유틸리티 최상위 (16~18점)
1. **Clad & Rene**: 서포터 역할 - 팀 생존력 최고 (힐/보호막/흡혈)
2. **Hilda & Sinobu**: 생존력 + 궁극기 우수
#### 유틸리티 중위권 (13~15점)
- **Cazimord**: 생존력 + 기동성 + 궁극기 (화력 중심)
- **Baran, Urud, Lian**: 궁극기로 부족한 점수 보충
- **Nave, Rio**: 화력 중심, 유틸리티 부족
---
## 7. 역할별 유틸리티 특성
### 전사 (Warriors)
| 스토커 | 유틸리티 특성 | 지속 DPS | 유틸리티 점수 |
|--------|---------------|----------|---------------|
| **Hilda** | 탱킹 (Blocking, 도발) + 궁극기 버프 | 117 | 16점 |
| **Baran** | CC (갈고리, 경직) + 궁극기 Stun | 128 | 14점 |
| **Cazimord** | 생존 (Parrying) + 궁극기 폭딜 | 221 | 15점 |
**차별화**:
- **Hilda**: 탱커 (Blocking, 도발)
- **Baran**: CC + Stun 궁극기
- **Cazimord**: 솔로 DPS (생존 + 화력)
---
### 원거리 (Ranged)
| 스토커 | 유틸리티 특성 | 지속 DPS | 유틸리티 점수 |
|--------|---------------|----------|---------------|
| **Urud** | CC (덫 Snare) + 궁극기 범위화 | 82 | 14점 |
| **Lian** | 궁극기 (무제한 화살) | 219 | 14점 |
**차별화**:
- **Urud**: CC 특화 (덫) + 범위 공격
- **Lian**: 고DPS + 궁극기 폭발력
---
### 마법사 (Mages)
| 스토커 | 유틸리티 특성 | 지속 DPS | 유틸리티 점수 |
|--------|---------------|----------|---------------|
| **Nave** | 궁극기 (관통 광역) | 202 | 13점 |
| **Rene** | 생존 (Lifesteal) + 팀 버프 (흡혈) | 148 | 18점 |
**차별화**:
- **Nave**: 광역 폭딜 특화
- **Rene**: 소환사 + 팀 서포터
---
### 암살자 (Assassins)
| 스토커 | 유틸리티 특성 | 지속 DPS | 유틸리티 점수 |
|--------|---------------|----------|---------------|
| **Rio** | 기동성 (돌진) + 궁극기 (은신) | 268 | 13점 |
| **Sinobu** | 기동성 + 생존 + 궁극기 (반사) | 176 | 16점 |
**차별화**:
- **Rio**: DPS 특화
- **Sinobu**: 기동성 + 생존 특화
---
### 서포터 (Support)
| 스토커 | 유틸리티 특성 | 지속 DPS | 유틸리티 점수 |
|--------|---------------|----------|---------------|
| **Clad** | 힐 + DOT 제거 + 보호막 | 76 | 18점 |
**역할**: 유일한 순수 서포터
- 치유 (파티 힐)
- 신성한 빛 (DOT 제거)
- 궁극기 (파티 보호막 300)
---
## 8. DPS vs 유틸리티 밸런스
### DPS-유틸리티 분포도
| 스토커 | 지속 DPS | 유틸리티 점수 | 포지션 |
|--------|----------|---------------|--------|
| **Rio** | 268 | 13 | 최고DPS 중유틸 ⚠️ |
| **Cazimord** | 221 | 15 | 고DPS 고유틸 |
| **Lian** | 219 | 14 | 고DPS 중유틸 |
| **Nave** | 202 | 13 | 고DPS 저유틸 |
| **Sinobu** | 176 | 16 | 중DPS 고유틸 ✅ |
| **Rene** | 148 | 18 | 중DPS 최고유틸 ✅ |
| **Baran** | 128 | 14 | 중DPS 중유틸 |
| **Hilda** | 117 | 16 | 중DPS 고유틸 ✅ |
| **Urud** | 82 | 14 | 저DPS 중유틸 ⚠️ |
| **Clad** | 76 | 18 | 저DPS 최고유틸 ✅ |
**밸런스 평가**:
#### 우수한 밸런스 ✅
- **Clad, Rene**: 낮은 DPS를 최고 유틸리티로 보상
- **Hilda, Sinobu**: 중간 DPS + 우수한 유틸리티
- **Cazimord, Lian**: 고DPS + 중간 유틸리티
#### 밸런스 이슈 ⚠️
- **Rio**: 최고 DPS (268) + 중간 유틸리티 → **과도한 화력** (2위보다 +21%)
- **Nave**: 고DPS (202) + 낮은 유틸리티 → DPS는 높지만 기본 스킬 유틸리티 부족
- **Urud**: 낮은 DPS (82) + 중간 유틸리티 → **버프 필요**
---
## 9. 최종 결론
### DPS + 유틸리티 종합 티어
| 티어 | 스토커 | 이유 |
|------|--------|------|
| **S+** | **Cazimord** | 고DPS (221) + 고유틸 (15점) + 버스트 1위 (256) |
| | **Rene** | 중DPS (148) + 최고유틸 (18점) + 팀 서포트 |
| **S** | Clad | 저DPS (76) + 최고유틸 (18점) - 서포터 |
| | Lian | 고DPS (219) + 중유틸 (14점) + 궁극기 폭발력 |
| | Hilda | 중DPS (117) + 고유틸 (16점) - 탱커 |
| **A** | Sinobu | 중DPS (176) + 고유틸 (16점) + 기동성 |
| | Nave | 고DPS (202) + 저유틸 (13점) + 광역 버스트 2위 (241) |
| | Baran | 중DPS (128) + 중유틸 (14점) + Stun 궁극기 |
| **B** | Urud | 저DPS (82) + 중유틸 (14점) ⚠️ 버프 필요 |
| **OP** | **Rio** | 최고DPS (268) + 중유틸 (13점) ⚠️ **너프 필요** |
---
### 개선 권장 사항
#### 1. Rio (암살자) - 너프 필요
**현재**: DPS 268 (압도적 1위) + 유틸리티 13점
**문제점**: 2위 Cazimord(221)보다 +21% 과도한 DPS
**권장 개선**:
- 스킬 쿨타임 약간 증가 (연속 찌르기 3.5초 → 4초)
- 또는 스킬 배율 소폭 감소 (1.0배 → 0.9배)
- **목표 DPS**: 230~240 정도 (Cazimord와 비슷한 수준)
#### 2. Urud (원거리 딜러) - 버프 필요
**현재**: DPS 82 (최하위) + 유틸리티 14점
**권장 개선**:
- Reload 시간 단축 (2초 → 1.5초)
- 또는 다발 화살 피해 배율 증가 (1.2 → 1.5)
- 또는 덫 설치 쿨타임 감소 (5초 → 4초)
- **목표 DPS**: 100~120 정도
#### 3. Nave (마법사) - 현상 유지
**현재**: DPS 202 (고DPS) + 유틸리티 13점
**평가**: 궁극기 광역 폭딜(241)로 상황적 우위 → **밸런스 양호**
---
**다음 단계**:
- **07_역할별_차별화.md**: 역할별 상세 비교 및 플레이스타일 분석
- **08_밸런스_티어_및_개선안.md**: 최종 티어표 및 전체 밸런스 개선 방안
---
**생성 일시**: 2025-10-24 01:00
**평가 기준**: 레벨 20, 기어스코어 400, 최적 플레이 (100% 활용)
**데이터 소스**: 03_스토커별_기본데이터.md, 04_DPS_계산_결과.md

View File

@ -0,0 +1,700 @@
# 07. 역할별 차별화 분석
## 개요
10명의 스토커는 5개 역할군으로 분류되며, 같은 역할군 내에서도 뚜렷한 차별화가 이루어져 있습니다.
**역할 분류**:
- **전사** (3명): Hilda, Baran, Cazimord
- **원거리** (2명): Urud, Lian
- **마법사** (2명): Nave, Rene
- **암살자** (2명): Rio, Sinobu
- **서포터** (1명): Clad
---
## 1. 전사 (Warriors) - 3명 비교
### 역할군 정의
전사는 근접 전투를 주도하는 역할로, 높은 STR/DEX 스탯과 근접 무기를 사용합니다.
### 공통점
| 항목 | 공통 특성 |
|------|----------|
| **무기 타입** | 근접 무기 (WeaponShield, TwoHandWeapon) |
| **공격 타입** | Physical 피해 |
| **룬 효과** | +10% 물리 피해 + +10% 스킬 피해 = 1.20배 |
| **평타 콤보** | 3타 콤보 |
| **역할** | 전선 유지, 적과의 근접 전투 |
### 상세 스탯 비교
| 스토커 | STR | DEX | INT | CON | WIS | BaseDamage | 평타 DPS | 지속 DPS | 버스트 DPS |
|--------|-----|-----|-----|-----|-----|------------|----------|----------|------------|
| **Hilda** | 20 | 15 | 10 | 20 | 10 | 120 | 144 | 117 | - |
| **Baran** | 25 | 10 | 5 | 25 | 10 | 126 | 164 | 128 | 184 |
| **Cazimord** | 15 | 25 | 10 | 15 | 10 | 126 | 184 | 221 | 256 |
**분석**:
- **BaseDamage**: Baran, Cazimord 동일 (126) > Hilda (120)
- **평타 DPS**: Cazimord (184) > Baran (164) > Hilda (144)
- **지속 DPS**: Cazimord (221) > Baran (128) > Hilda (117)
- **버스트 DPS**: Cazimord (256) > Baran (184)
### 스킬 구성 비교
| 스토커 | 기본 스킬 1 | 기본 스킬 2 | 기본 스킬 3 | 서브 스킬 | 궁극기 |
|--------|-------------|-------------|-------------|----------|--------|
| **Hilda** | 칼날 찌르기<br>(1.3배, 6초) | 반격<br>(1.2배, 4초) | 도발<br>(어그로) | Blocking<br>(100% 물리 차단) | 핏빛 달<br>(공/방 버프, 20초) |
| **Baran** | 갈고리 투척<br>(0.25배, 13초) | 후려치기<br>(1.2배, 8초) | 깊게 찌르기<br>(1.1배, 7초) | 무기 막기<br>(방어) | 일격분쇄<br>(1.7배 + Stun 3초) |
| **Cazimord** | 섬광<br>(0.5배, 15.5초) | 날개베기<br>(0.3배, 15.5초) | 작열<br>(+20%, 27.5초) | Parrying<br>(0.2초 판정) | 칼날폭풍<br>(12연타, 10.0배) |
**스킬 특징**:
- **Hilda**: 짧은 쿨타임 (4~6초), 유틸리티 중심
- **Baran**: 중간 쿨타임 (7~13초), CC 보유
- **Cazimord**: 긴 쿨타임 (15.5~27.5초), 평타 중심 설계
### 차별화 포인트
#### Hilda - 방어형 탱커
**핵심 시스템**: Blocking (100% 물리 차단, 90% 마법 차단)
| 특징 | 내용 |
|------|------|
| **강점** | 최고 생존력, 어그로 관리 (도발) |
| **약점** | 낮은 DPS (117), 버스트 부족 |
| **플레이스타일** | Blocking으로 피해 차단, 도발로 적 유도, 반격(4초)으로 꾸준한 딜 |
| **궁극기** | 핏빛 달 (공격력 +15, 방어력 +25, 20초) - 장시간 버프 |
| **유틸리티** | ⭐⭐⭐⭐⭐ (16점) - 탱킹, 어그로 관리 |
| **추천 상황** | 파티 탱커, 보스전 전선 유지, 아군 보호 |
**Counter vs Parrying 비교**:
- **Hilda Counter**: 0.5초 판정 → 2.5배 쉬움
- **Cazimord Parrying**: 0.2초 판정 → 어려움, BUT 쿨타임 감소 보상
---
#### Baran - CC 특화 전사
**핵심 시스템**: 갈고리 + 경직 + Stun 궁극기
| 특징 | 내용 |
|------|------|
| **강점** | CC 다수 (갈고리 Pull, 경직, Stun 3초), 높은 STR/CON (25/25) |
| **약점** | 낮은 지속 DPS (128), 스킬 배율 낮음 (갈고리 0.25배) |
| **플레이스타일** | 갈고리로 적 끌어당김 → 후려치기/깊게 찌르기 연계 → 궁극기 Stun |
| **궁극기** | 일격분쇄 (1.7배 + Stun 3초 + 광역) - **유일한 Hard CC 궁극기** |
| **유틸리티** | ⭐⭐⭐⭐ (14점) - CC 특화 |
| **추천 상황** | CC 필요 시, 적 제압, 팀 집중 포화 지원 |
**문제점**:
- "파워 전사" 컨셉이지만 DPS는 중하위권 (128)
- 갈고리 배율 너무 낮음 (0.25배)
---
#### Cazimord - 고숙련도 DPS 전사 ⭐
**핵심 시스템**: Parrying (0.2초 판정) + 평타 중심 설계
| 특징 | 내용 |
|------|------|
| **강점** | 전사 최고 DPS (221), 버스트 1위 (256), 높은 스킬 캡 |
| **약점** | Parrying 난이도 높음, CC 없음 |
| **플레이스타일** | 평타 중심 (90% 비중), Parrying으로 쿨타임 단축, Burn 버프 극대화 |
| **궁극기** | 칼날폭풍 (12연타, 10.0배) - **단일 대상 최강 버스트** |
| **유틸리티** | ⭐⭐⭐⭐⭐ (15점) - 생존력 (Parrying) |
| **추천 상황** | 솔로 플레이, 보스 킬, 고숙련 유저 |
**Parrying 효과** (왜곡 룬 -25% + 패링 감소):
- **섬광**: 15.5초 → 11.6초 → **7.8초** (100% 패링 시)
- **날개베기**: 15.5초 → 11.6초 → **7.8초**
- **작열**: 27.5초 → 20.6초 → **13.8초**
**DPS 격차**:
- 패링 0%: 166 DPS
- 패링 100%: 221 DPS
- **차이**: +55 DPS (+33%)
### 전사 역할별 포지셔닝
```
높은 DPS
Cazimord (221)
|
|
Baran (128)
|
Hilda (117)
|
낮은 DPS
```
```
높은 유틸리티 ← Hilda (16점, 탱커)
|
Cazimord (15점, 생존)
|
Baran (14점, CC) → 낮은 유틸리티
```
### 추천 상황별 선택
| 상황 | 추천 스토커 | 이유 |
|------|------------|------|
| **파티 탱커** | **Hilda** | Blocking + 도발, 최고 생존력 |
| **보스 킬** | **Cazimord** | 버스트 1위 (256), 지속 DPS 1위 (221) |
| **적 제압/CC** | **Baran** | Stun 3초 궁극기, 갈고리 Pull |
| **솔로 플레이** | **Cazimord** | 고DPS + Parrying 생존 |
| **초보자** | **Hilda** | Blocking 쉬움, 안정적 |
| **고숙련자** | **Cazimord** | Parrying 고난이도, 고보상 |
---
## 2. 원거리 (Ranged) - 2명 비교
### 역할군 정의
원거리는 Bow를 사용하는 물리 딜러로, 안전한 거리에서 지속 피해를 가합니다.
### 공통점
| 항목 | 공통 특성 |
|------|----------|
| **무기 타입** | Bow |
| **공격 타입** | Physical 피해 |
| **룬 효과** | +10% 물리 피해 + +10% 스킬 피해 = 1.20배 |
| **특수 시스템** | Reload (탄약 6발, 재장전 2초) |
| **평타** | 1타 반복 |
| **약점** | Reload 페널티로 평타 DPS 감소 |
### 상세 스탯 비교
| 스토커 | STR | DEX | INT | CON | WIS | BaseDamage | 평타 DPS | 지속 DPS | 버스트 DPS |
|--------|-----|-----|-----|-----|-----|------------|----------|----------|------------|
| **Urud** | 15 | 20 | 10 | 15 | 15 | 120 | 90 | 82 | - |
| **Lian** | 10 | 20 | 10 | 15 | 20 | 120 | 77 | 219 | - |
**분석**:
- **BaseDamage**: 동일 (120)
- **평타 DPS**: Urud (90) > Lian (77) - Lian은 충전 시간 추가
- **지속 DPS**: Lian (219) >>> Urud (82) - **2.7배 차이**
**DPS 역전 이유**:
- **Lian**: 속사 스킬 (0.85배 × 4발 × 만충전 1.5배 = 5.1배 상당) - 초강력
- **Urud**: 스킬 배율 낮음 (다발 화살 1.2배, 독침 화살 0.8배)
### 스킬 구성 비교
| 스토커 | 기본 스킬 1 | 기본 스킬 2 | 기본 스킬 3 | 기본 스킬 4 | 서브 | 궁극기 |
|--------|-------------|-------------|-------------|-------------|------|--------|
| **Urud** | 다발 화살<br>(1.2배, 7초) | 독침 화살<br>(0.8배, 7초) | 덫 설치<br>(Snare 3초, 5초) | Reload<br>(2초) | 화살 발사<br>(평타) | 폭쇄<br>(범위화, 15초) |
| **Lian** | 속사<br>(0.85×4발, 7초) | 비연사<br>(1.5배, 10초) | 연화<br>(1.2배, 7.5초) | 재장전<br>(2초) | 정조준<br>(피해 증가) | 폭우<br>(무제한 화살, 15초) |
**스킬 특징**:
- **Urud**: CC 보유 (덫 Snare 3초), 독침 DOT
- **Lian**: 속사 4발 고배율, 만충전 시스템
### 차별화 포인트
#### Urud - CC 특화 원거리
**핵심 시스템**: 덫 설치 (Snare 3초, 쿨타임 5초)
| 특징 | 내용 |
|------|------|
| **강점** | CC 우수 (덫 Snare), 궁극기 범위화 (15초간 스플래시) |
| **약점** | 최하위 DPS (82), 스킬 배율 낮음 |
| **플레이스타일** | 덫으로 적 속박 → 다발/독침 화살 → 거리 유지 |
| **궁극기** | 폭쇄 (화살 범위화 + 화상 30%, 15초) - 몹 그룹 상대 시 유용 |
| **유틸리티** | ⭐⭐⭐⭐ (14점) - CC 특화 |
| **추천 상황** | 적 제압, 몹 그룹 상대 (궁극기), 유틸리티 필요 시 |
**문제점**:
- DPS 82는 너무 낮음 (Clad 서포터 76과 비슷)
- 다발 화살 배율 1.2배로는 부족
---
#### Lian - 고화력 레인저
**핵심 시스템**: Charging Bow (만충전 1.5배) + 속사 4발
| 특징 | 내용 |
|------|------|
| **강점** | 원거리 최고 DPS (219), 속사 4발 고배율 (0.85×4×1.5 = 5.1배) |
| **약점** | 낮은 유틸리티 (10점 최하위), 충전 시간 필요 |
| **플레이스타일** | 만충전 → 속사 4발 → 연화/비연사 → 재장전 → 반복 |
| **궁극기** | 폭우 (화살 무제한 + 쿨타임 감소, 15초) - **Reload 제약 완전 제거** |
| **유틸리티** | ⭐⭐ (14점) - 유틸리티 부족, DPS로 보상 |
| **추천 상황** | 고화력 필요 시, 보스 킬, 안전한 후방 딜러 |
**속사 스킬 분석**:
```
속사 = 0.85배 × 4발 × 만충전 1.5배 = 5.1배 상당
→ 쿨타임 7초 (왜곡 룬 시 5.25초)
→ 단일 스킬 중 최고 효율
```
**궁극기 효과**:
- Reload 제약 제거 → 평타 DPS 77 → ~150 추정
- 스킬 쿨타임 감소 → 속사 난사
- 15초간 폭발적 DPS
### 원거리 역할별 포지셔닝
```
높은 DPS
Lian (219)
|
|
|
|
Urud (82)
낮은 DPS
```
```
높은 유틸리티 ← Urud (14점, CC)
|
|
Lian (14점, DPS) → 낮은 유틸리티
```
**유틸리티 점수는 같지만 내용이 다름**:
- **Urud**: CC (덫), 궁극기 범위화
- **Lian**: 궁극기 화력 폭발
### 추천 상황별 선택
| 상황 | 추천 스토커 | 이유 |
|------|------------|------|
| **고화력 필요** | **Lian** | DPS 219, 속사 4발 |
| **적 제압/CC** | **Urud** | 덫 Snare 3초 |
| **몹 그룹** | **Urud** | 궁극기 범위화 15초 |
| **보스 킬** | **Lian** | 고DPS + 궁극기 폭발력 |
| **초보자** | **Lian** | 만충전 후 속사만 써도 강함 |
| **유틸리티 중시** | **Urud** | CC 보유 |
---
## 3. 마법사 (Mages) - 2명 비교
### 역할군 정의
마법사는 Staff를 사용하는 마법 딜러로, INT/WIS 스탯이 높으며 광역 마법 공격을 사용합니다.
### 공통점
| 항목 | 공통 특성 |
|------|----------|
| **무기 타입** | Staff |
| **공격 타입** | Magical 피해 |
| **룬 효과** | +10% 마법 피해 = 1.10배 (물리보다 낮음) |
| **역할** | 광역 딜러, 소환수/정령 |
### 상세 스탯 비교
| 스토커 | STR | DEX | INT | CON | WIS | BaseDamage | 평타 DPS | 지속 DPS | 버스트 DPS |
|--------|-----|-----|-----|-----|-----|------------|----------|----------|------------|
| **Nave** | 10 | 10 | 25 | 10 | 20 | 115.5 | 115 | 202 | 241 |
| **Rene** | 10 | 10 | 20 | 10 | 25 | 110 | 132 | 148 | - |
**분석**:
- **BaseDamage**: Nave (115.5) > Rene (110)
- **평타 DPS**: Rene (132) > Nave (115) - Rene는 3타 콤보
- **지속 DPS**: Nave (202) > Rene (148)
- **버스트 DPS**: Nave (241) 압도적
### 스킬 구성 비교
| 스토커 | 기본 스킬 1 | 기본 스킬 2 | 기본 스킬 3 | 서브 | 궁극기 |
|--------|-------------|-------------|-------------|------|--------|
| **Nave** | 마법 화살<br>(0.8×3발, 3.5초) | 화염구<br>(2.0배, 5초) | 노대바람<br>(0.5배, 7초) | 마력 충전<br>(마나 회복) | 해방<br>(관통 광선, 10.0배) |
| **Rene** | 정령 소환: 화염<br>(1.2배, 7초) | 정령 소환: 냉기<br>(0.8배, 10초) | 독기 화살<br>(1.0배, 10초) | 할퀴기<br>(Lifesteal) | 붉은 축제<br>(파티 흡혈, 20초) |
**스킬 특징**:
- **Nave**: 짧은 쿨타임 (3.5~7초), 직접 공격형
- **Rene**: 소환수 중심, 장시간 지속 (정령)
### 차별화 포인트
#### Nave - 광역 폭딜 마법사
**핵심 시스템**: 화염구 2.0배 고배율 + 궁극기 관통 광선
| 특징 | 내용 |
|------|------|
| **강점** | 버스트 2위 (241), 짧은 쿨타임, **궁극기 관통 광역 (10.0배)** |
| **약점** | 낮은 유틸리티 (13점), 생존력 부족 |
| **플레이스타일** | 화염구(2.0) → 마법 화살(0.8×3) → 노대바람 → 반복 |
| **궁극기** | 해방 (관통 광선, 1.0배×10회, 5초) - **다수 적 상대 시 Cazimord 초월** |
| **유틸리티** | ⭐⭐⭐ (13점) - 노대바람 Knockback만 |
| **추천 상황** | 몹 그룹, 직선 배치 적, 광역 폭딜 |
**궁극기 '해방' 분석**:
- 직선상 모든 적에게 0.5초마다 10회 타격
- 단일 적: 2,414 피해
- 3명 적중 시: **7,242** 피해 (이론값)
- **Cazimord 칼날폭풍(단일 2,560)을 상황적으로 초월**
**화염구 효율**:
```
화염구 = 2.0배, 쿨타임 5초 (왜곡 룬 시 3.75초)
→ 마법 스킬 중 최고 배율
→ Nave 지속 DPS의 핵심
```
---
#### Rene - 소환사 서포터
**핵심 시스템**: 정령 소환 (Ifrit, Shiva) + Lifesteal
| 특징 | 내용 |
|------|------|
| **강점** | 생존력 (Lifesteal + 궁극기 흡혈), 팀 기여 (파티 흡혈 20초) |
| **약점** | 중간 DPS (148), 소환수 관리 필요 |
| **플레이스타일** | 정령 소환 → 독기 화살 → 할퀴기 회복 → 소환수 자동 공격 |
| **궁극기** | 붉은 축제 (파티 흡혈, 20초) - **팀 생존력 극대화** |
| **유틸리티** | ⭐⭐⭐⭐⭐ (18점) - 생존 + 팀 버프 최고 |
| **추천 상황** | 파티 플레이, 장기전, 생존력 중시 |
**소환수 시스템**:
- **화염 정령 (Ifrit)**: 고정 위치, 화염 화살 발사
- **냉기 정령 (Shiva)**: Rene 추종, 얼음 송곳 발사
- 소환수는 자동 공격, 추가 DPS 제공 (약 140 DPS 추정)
**Lifesteal 효과**:
- 할퀴기: 자체 흡혈
- 궁극기: 파티 전체 흡혈 20초
- → 장기전에서 힐러 없이도 생존 가능
### 마법사 역할별 포지셔닝
```
높은 DPS
Nave (202)
|
|
Rene (148)
낮은 DPS
```
```
높은 유틸리티 ← Rene (18점, 서포트)
|
|
Nave (13점, 화력) → 낮은 유틸리티
```
### 추천 상황별 선택
| 상황 | 추천 스토커 | 이유 |
|------|------------|------|
| **광역 폭딜** | **Nave** | 궁극기 관통 광선 (10.0배) |
| **몹 그룹** | **Nave** | 화염구 + 궁극기 광역 |
| **보스 킬** | **Nave** | 버스트 2위 (241) |
| **파티 플레이** | **Rene** | 궁극기 파티 흡혈 (20초) |
| **장기전** | **Rene** | Lifesteal 생존 |
| **솔로 플레이** | **Rene** | 할퀴기 흡혈 + 소환수 |
| **초보자** | **Nave** | 화염구만 써도 강함 |
---
## 4. 암살자 (Assassins) - 2명 비교
### 역할군 정의
암살자는 ShortSword를 사용하는 빠른 근접 딜러로, 높은 DEX와 기동성이 특징입니다.
### 공통점
| 항목 | 공통 특성 |
|------|----------|
| **무기 타입** | ShortSword |
| **공격 타입** | Physical 피해 |
| **룬 효과** | +10% 물리 피해 + +10% 스킬 피해 = 1.20배 |
| **주 스탯** | DEX 25 (최고) |
| **역할** | 빠른 근접 딜러, 기동성 |
### 상세 스탯 비교
| 스토커 | STR | DEX | INT | CON | WIS | BaseDamage | 평타 DPS | 지속 DPS | 버스트 DPS |
|--------|-----|-----|-----|-----|-----|------------|----------|----------|------------|
| **Rio** | 15 | 25 | 10 | 15 | 10 | 126 | 196 | 268 | 200 |
| **Sinobu** | 10 | 25 | 10 | 15 | 15 | 126 | 151 | 176 | 196 |
**분석**:
- **BaseDamage**: 동일 (126)
- **평타 DPS**: Rio (196) > Sinobu (151) - Rio 3타 vs Sinobu 2타
- **지속 DPS**: Rio (268) >>> Sinobu (176) - **1.5배 차이**
- **버스트 DPS**: Rio (200) ≈ Sinobu (196)
### 스킬 구성 비교
| 스토커 | 기본 스킬 1 | 기본 스킬 2 | 기본 스킬 3 | 서브 | 궁극기 |
|--------|-------------|-------------|-------------|------|--------|
| **Rio** | 연속 찌르기<br>(1.0×2회, 3.5초) | 접근<br>(1.0배 돌진, 4초) | 단검 투척<br>(1.0배, 7초) | 내려 찍기<br>(Chain Score) | 민감<br>(Chain 3점, 15초) |
| **Sinobu** | 기폭찰<br>(1.3배, 6초) | 비뢰각<br>(1.1배, 8초) | 바꿔치기<br>(0.9배, 11초) | 표창<br>(0.8배, 충전 3개) | 반환<br>(반사+막기, 7초) |
**스킬 특징**:
- **Rio**: 짧은 쿨타임 (3.5~7초), Chain Score 시스템
- **Sinobu**: 충전 시스템 (표창 3개), 방어 궁극기
### 차별화 포인트
#### Rio - DPS 특화 암살자 ⚠️
**핵심 시스템**: Chain Score (최대 3스택) + 초짧은 쿨타임
| 특징 | 내용 |
|------|------|
| **강점** | **압도적 DPS (268, 전체 1위)**, 평타 1위 (196), 기동성 (접근 4초) |
| **약점** | 낮은 유틸리티 (13점), CC 없음 |
| **플레이스타일** | 연속 찌르기(3.5초) → 접근(4초) → 단검 투척 → 반복 (초고속 로테이션) |
| **궁극기** | 민감 (Chain Score 3점 + 은신 + 투시, 15초) - 전술 강화 |
| **유틸리티** | ⭐⭐⭐ (13점) - 기동성만 우수 |
| **밸런스** | ⚠️ **과도한 DPS** (2위보다 +21%) → 너프 필요 |
**DPS 압도 이유**:
1. **짧은 쿨타임**: 연속 찌르기 2.6초 (왜곡 룬), 접근 3초
2. **높은 평타 DPS**: 196 (빠른 3타 콤보)
3. **스킬 회전율**: 30초간 연속 찌르기 10회, 접근 9회
**Chain Score 시스템**:
- 스킬 사용 시 1점 획득 (최대 3점)
- 3점 충전 시 내려 찍기 강화
- 궁극기로 즉시 3점 충전 가능
---
#### Sinobu - 기동성 특화 닌자
**핵심 시스템**: Swap (텔레포트) + 표창 충전 + 방어 궁극기
| 특징 | 내용 |
|------|------|
| **강점** | 우수한 기동성 (바꿔치기), 생존력 (궁극기 반사+막기), 유틸리티 (16점) |
| **약점** | 중간 DPS (176), 표창 관리 필요 |
| **플레이스타일** | 기폭찰 → 표창×3 → 비뢰각 → 바꿔치기 (회피) → 반복 |
| **궁극기** | 반환 (투사체 반사 + 근접 막기, 7초) - **생존형 궁극기** |
| **유틸리티** | ⭐⭐⭐⭐ (16점) - 기동성 + 생존 |
| **추천 상황** | 위험 상황 대응, 원거리 적 카운터, 전술적 플레이 |
**표창 충전 시스템**:
- 최대 3개 충전
- 1초/개 자동 충전
- 0.8배 피해 (빠른 발사)
**바꿔치기 효과**:
- 사용 후 피격 시 피해 감소
- 투명화 (적 타겟 해제)
- 이동속도 증가
- → 생존 + 재배치
**궁극기 '반환' 분석**:
- 전방 투사체 반사 (원거리 적 카운터)
- 근접 공격 막기
- 7초간 무적에 가까움
- 원거리 보스전에서 강력
### 암살자 역할별 포지셔닝
```
높은 DPS
Rio (268) ⚠️
|
|
Sinobu (176)
낮은 DPS
```
```
높은 유틸리티 ← Sinobu (16점, 생존+기동)
|
|
Rio (13점, 화력) → 낮은 유틸리티
```
### 추천 상황별 선택
| 상황 | 추천 스토커 | 이유 |
|------|------------|------|
| **고화력 필요** | **Rio** | DPS 268 압도적 1위 |
| **빠른 킬** | **Rio** | 평타 196 + 짧은 쿨타임 |
| **원거리 보스** | **Sinobu** | 궁극기 투사체 반사 |
| **위험 상황** | **Sinobu** | 바꿔치기 회피 + 궁극기 방어 |
| **솔로 플레이** | **Sinobu** | 생존력 우수 |
| **파티 플레이** | **Rio** | 압도적 DPS 기여 (BUT 너프 예정) |
| **초보자** | **Sinobu** | 궁극기 방어로 실수 만회 |
| **고숙련자** | **Rio** | 초짧은 쿨타임 완벽 활용 |
---
## 5. 서포터 (Support) - Clad
### 역할군 정의
Clad는 유일한 순수 서포터로, 힐링과 팀 버프를 제공합니다.
### 기본 정보
| 항목 | 내용 |
|------|------|
| **무기 타입** | Mace |
| **주 스탯** | CON 20, WIS 20 |
| **BaseDamage** | 95 (룬 효과 없음) |
| **평타 DPS** | 86 |
| **지속 DPS** | 76 (최하위) |
| **유틸리티** | 18점 (최고, Rene와 동점) |
### 스킬 구성
| 스킬 | 효과 | 쿨타임 |
|------|------|--------|
| **치유** | 파티 HP 회복 (1.0배) | 3초 |
| **다시 흙으로** | 범위 피해 (1.5배, Holy) | 5초 |
| **신성한 빛** | 파티 DOT 제거 | 7.5초 |
| **방패 방어** (서브) | 방어 | 0초 |
| **황금** (궁극기) | 파티 보호막 300 | 쿨타임 미표기 |
### 역할 및 강점
| 특징 | 내용 |
|------|------|
| **강점** | 유일한 힐러, DOT 제거, 보호막 300 (초강력) |
| **약점** | 최하위 DPS (76), 공격 스킬 1개만 |
| **플레이스타일** | 치유 반복 → DOT 제거 → 다시 흙으로 (틈날 때) |
| **궁극기** | 황금 (파티 보호막 300) - **위기 상황 구원** |
| **필수 상황** | 힐러 필요 던전, 고난이도 레이드, DOT 보스 |
### 다른 서포터와의 비교
Rene도 서포터 역할을 할 수 있지만, Clad와 차이가 있습니다:
| 항목 | Clad | Rene |
|------|------|------|
| **역할** | 순수 서포터 (힐러) | 서포터형 딜러 |
| **DPS** | 76 (최하위) | 148 (중위) |
| **힐링** | 직접 힐 (3초 쿨타임) | 흡혈 (궁극기 20초) |
| **팀 기여** | 힐 + DOT 제거 + 보호막 | 흡혈 버프 (20초) |
| **유틸리티** | 18점 | 18점 (동점) |
| **추천** | 힐러 필수 상황 | 힐러 없어도 버틸 수 있는 상황 |
### 밸런스 평가
Clad는 낮은 DPS (76)를 최고 유틸리티 (18점)로 보상하므로 **밸런스 양호**합니다.
---
## 6. 역할군 간 비교
### DPS 순위 (역할군별)
| 순위 | 스토커 | 역할 | 지속 DPS | 버스트 DPS |
|------|--------|------|----------|------------|
| 1 | **Rio** | 암살자 | **268** ⚠️ | 200 |
| 2 | **Cazimord** | 전사 | 221 | **256** |
| 3 | **Lian** | 원거리 | 219 | - |
| 4 | **Nave** | 마법사 | 202 | **241** |
| 5 | **Sinobu** | 암살자 | 176 | 196 |
| 6 | **Rene** | 마법사 | 148 | - |
| 7 | **Baran** | 전사 | 128 | 184 |
| 8 | **Hilda** | 전사 | 117 | - |
| 9 | **Urud** | 원거리 | 82 ⚠️ | - |
| 10 | **Clad** | 서포터 | 76 | - |
### 역할군별 평균 DPS
| 역할군 | 평균 지속 DPS | 최고 | 최저 | 격차 |
|--------|---------------|------|------|------|
| **암살자** | 222 | Rio (268) | Sinobu (176) | 1.5배 |
| **원거리** | 151 | Lian (219) | Urud (82) | 2.7배 |
| **마법사** | 175 | Nave (202) | Rene (148) | 1.4배 |
| **전사** | 155 | Cazimord (221) | Hilda (117) | 1.9배 |
**분석**:
- **암살자**: 평균 DPS 최고 (222), Rio 과다
- **원거리**: 격차 최대 (2.7배), Urud 버프 필요
- **전사**: 격차 큼 (1.9배), 역할 차별화 명확
### 유틸리티 순위 (역할군별)
| 순위 | 스토커 | 역할 | 유틸리티 점수 | 특징 |
|------|--------|------|---------------|------|
| 1 | **Clad** | 서포터 | 18점 | 힐 + 보호막 |
| 1 | **Rene** | 마법사 | 18점 | 흡혈 버프 |
| 3 | **Hilda** | 전사 | 16점 | 탱킹 |
| 3 | **Sinobu** | 암살자 | 16점 | 생존 + 기동 |
| 5 | **Cazimord** | 전사 | 15점 | 생존 (Parrying) |
| 6 | **Baran** | 전사 | 14점 | CC |
| 6 | **Urud** | 원거리 | 14점 | CC |
| 6 | **Lian** | 원거리 | 14점 | 궁극기 |
| 9 | **Nave** | 마법사 | 13점 | 궁극기 |
| 9 | **Rio** | 암살자 | 13점 | 기동성 |
### 역할군별 특성 요약
| 역할군 | DPS | 유틸리티 | 생존력 | 기동성 | 추천 플레이어 |
|--------|-----|----------|--------|--------|---------------|
| **전사** | 중~고 | 중~고 | 고 | 저~중 | 전선 유지, 근접 전투 선호 |
| **원거리** | 저~고 | 중 | 저 | 저~중 | 안전한 후방, 정밀 조준 |
| **마법사** | 중~고 | 중~최고 | 저~중 | 저 | 광역 공격, 전략적 플레이 |
| **암살자** | 고~최고 | 중 | 중~고 | 최고 | 기동전, 빠른 전투 |
| **서포터** | 최저 | 최고 | 고 | 저 | 팀 플레이, 힐러 역할 |
---
## 7. 최종 권장 사항
### 파티 구성 추천
#### 균형 파티 (3인)
- **탱커**: Hilda
- **힐러**: Clad
- **딜러**: Cazimord or Lian
#### 고화력 파티 (3인)
- **딜러 1**: Rio (DPS 268) ⚠️
- **딜러 2**: Cazimord (버스트 256)
- **서포터**: Rene (흡혈 버프)
#### 몹 그룹 파티 (3인)
- **광역 1**: Nave (궁극기 관통)
- **광역 2**: Urud (궁극기 범위화)
- **힐러**: Clad
### 솔로 플레이 추천
| 순위 | 스토커 | 이유 |
|------|--------|------|
| 1 | **Cazimord** | 고DPS + Parrying 생존 |
| 2 | **Sinobu** | 궁극기 방어 + 기동성 |
| 3 | **Rene** | Lifesteal + 소환수 |
| 4 | **Lian** | 고DPS + 원거리 안전 |
### 밸런스 개선 우선순위
| 우선순위 | 스토커 | 문제점 | 개선 방향 |
|----------|--------|--------|-----------|
| **최우선** | **Rio** | DPS 과다 (268) | 스킬 쿨타임 증가 or 배율 감소 → 목표 230~240 |
| **우선** | **Urud** | DPS 부족 (82) | Reload 단축 or 스킬 배율 증가 → 목표 100~120 |
| **검토** | **Baran** | "파워 전사"인데 낮은 DPS (128) | 갈고리 배율 증가 (0.25 → 0.5) 검토 |
---
**생성 일시**: 2025-10-24 01:30
**분석 기준**: 레벨 20, 기어스코어 400, 최적 플레이 (100% 활용)
**데이터 소스**: 03_스토커별_기본데이터.md, 04_DPS_계산_결과.md, 06_유틸리티_평가.md

View File

@ -0,0 +1,741 @@
# 08. 밸런스 티어 및 개선안
## 개요
10명의 스토커에 대한 종합 분석을 바탕으로 최종 티어를 정하고, 밸런스 개선안을 제시합니다.
**분석 기준**:
- 레벨 20, 기어스코어 400
- 최적 플레이 (100% 활용)
- DPS + 유틸리티 종합 평가
---
## 1. 종합 티어표
### 1.1. DPS + 유틸리티 종합 티어
**평가 기준**: 지속 DPS, 버스트 DPS, 유틸리티, 역할 기여도 종합
| 티어 | 스토커 | 지속 DPS | 유틸리티 | 주요 강점 | 밸런스 상태 |
|------|--------|----------|----------|-----------|-------------|
| **OP** | **Rio** | **268** | 13점 | 압도적 DPS, 짧은 쿨타임, 기동성 | ⚠️ **너프 필요** |
| **S+** | **Cazimord** | 221 | 15점 | 버스트 1위 (256), Parrying, 고숙련 보상 | ✅ 양호 |
| | **Rene** | 148 | 18점 | 팀 서포터, 파티 흡혈 20초, Lifesteal | ✅ 양호 |
| **S** | **Clad** | 76 | 18점 | 유일한 힐러, 보호막 300 | ✅ 양호 |
| | **Lian** | 219 | 14점 | 고DPS, 속사 4발, 궁극기 폭발력 | ✅ 양호 |
| | **Nave** | 202 | 13점 | 광역 버스트 2위 (241), 관통 궁극기 | ✅ 양호 |
| | **Hilda** | 117 | 16점 | 탱커, Blocking 100%, 어그로 관리 | ✅ 양호 |
| **A** | **Sinobu** | 176 | 16점 | 기동성, 궁극기 반사+막기 | ✅ 양호 |
| | **Baran** | 128 | 14점 | CC 특화, Stun 3초 궁극기 | 🔶 개선 검토 |
| **B** | **Urud** | 82 | 14점 | CC (덫), 궁극기 범위화 | ⚠️ **버프 필요** |
**티어 설명**:
- **OP** (Overpowered): 과도한 성능, 즉시 조정 필요
- **S+**: 최상위, 역할 모델
- **S**: 상위, 경쟁력 우수
- **A**: 중상위, 밸런스 양호
- **B**: 중하위, 개선 필요
---
### 1.2. 지속 DPS 티어
순수 DPS 기준 (30초 스킬 로테이션)
| 티어 | 스토커 | 지속 DPS | 역할 | 비고 |
|------|--------|----------|------|------|
| **S+** | Rio | **268** | 암살자 | ⚠️ 2위보다 +21% 과다 |
| **S** | Cazimord | 221 | 전사 | 패링 100% 기준 |
| | Lian | 219 | 원거리 | 속사 4발 시너지 |
| | Nave | 202 | 마법사 | 화염구 2.0배 고배율 |
| **A** | Sinobu | 176 | 암살자 | 균형잡힌 DPS |
| | Rene | 148 | 마법사 | 소환수 DPS 포함 |
| **B** | Baran | 128 | 전사 | 스킬 배율 낮음 |
| | Hilda | 117 | 전사 | 탱커 역할 |
| **C** | Urud | 82 | 원거리 | ⚠️ Reload 페널티 과다 |
| | Clad | 76 | 서포터 | 힐러 역할 |
**DPS 격차 분석**:
- 1위 Rio (268) vs 2위 Cazimord (221): **+47 (+21%)**
- 2위 Cazimord (221) vs 9위 Urud (82): **2.7배**
- 서포터 Clad 제외 시 격차: Rio vs Urud = **3.3배**
---
### 1.3. 버스트 DPS 티어
10초 풀콤보 기준 (궁극기 포함)
| 티어 | 스토커 | 버스트 DPS | 궁극기 | 비고 |
|------|--------|------------|--------|------|
| **S+** | **Cazimord** | **256** | 칼날폭풍 (10.0배, 12연타) | 단일 대상 최강 |
| | **Nave** | **241** | 해방 (10.0배, 관통) | 다수 적 상대 시 최강 |
| **S** | Rio | 200 | 민감 (Chain 3점) | 지속 DPS와 균형 |
| | Sinobu | 196 | 반환 (반사+막기) | 방어형 궁극기 |
| **A** | Baran | 184 | 일격분쇄 (1.7배 + Stun) | CC 포함 |
| **-** | Hilda | - | 핏빛 달 (버프) | 피해 없음 |
| | Urud | - | 폭쇄 (범위화) | 피해 없음 |
| | Clad | - | 황금 (보호막) | 피해 없음 |
| | Rene | - | 붉은 축제 (흡혈) | 피해 없음 |
| | Lian | - | 폭우 (무제한 화살) | 피해 없음 |
**버스트 특징**:
- **Cazimord & Nave**: 둘 다 10.0배 총 배율
- Cazimord: 단일 집중 (12연타)
- Nave: 광역 관통 (직선상 모든 적 10회)
- **상황별 최강**: 보스 = Cazimord, 몹 그룹 = Nave
---
### 1.4. 유틸리티 티어
CC, 생존력, 기동성, 팀 기여, 궁극기 종합
| 티어 | 스토커 | 유틸리티 점수 | 주요 유틸리티 |
|------|--------|---------------|---------------|
| **S+** | Clad | 18점 | 힐 + DOT 제거 + 보호막 300 |
| | Rene | 18점 | Lifesteal + 파티 흡혈 20초 |
| **S** | Hilda | 16점 | Blocking 100% + 도발 |
| | Sinobu | 16점 | 기동성 + 궁극기 반사+막기 |
| **A** | Cazimord | 15점 | Parrying + 생존력 |
| | Baran | 14점 | CC (갈고리, Stun 3초) |
| | Urud | 14점 | CC (덫 Snare 3초) |
| | Lian | 14점 | 궁극기 (무제한 화살) |
| **B** | Nave | 13점 | 궁극기 광역 관통 |
| | Rio | 13점 | 기동성 (돌진 4초) |
---
### 1.5. 역할별 티어
각 역할군 내에서의 순위
#### 전사 (Warriors)
| 순위 | 스토커 | 종합 평가 |
|------|--------|-----------|
| 1위 | **Cazimord** | S+ (고DPS + 고유틸 + 버스트 1위) |
| 2위 | **Hilda** | S (탱커 역할 완벽) |
| 3위 | **Baran** | A (CC 특화, DPS 낮음) |
#### 원거리 (Ranged)
| 순위 | 스토커 | 종합 평가 |
|------|--------|-----------|
| 1위 | **Lian** | S (고DPS + 궁극기) |
| 2위 | **Urud** | B (CC 좋지만 DPS 과도하게 낮음) |
#### 마법사 (Mages)
| 순위 | 스토커 | 종합 평가 |
|------|--------|-----------|
| 1위 | **Rene** | S+ (서포터 역할 + 중DPS) |
| 2위 | **Nave** | S (광역 폭딜 + 버스트 2위) |
#### 암살자 (Assassins)
| 순위 | 스토커 | 종합 평가 |
|------|--------|-----------|
| 1위 | **Rio** | OP (압도적 DPS, 너프 필요) |
| 2위 | **Sinobu** | A (균형잡힌 DPS + 유틸) |
#### 서포터 (Support)
| 순위 | 스토커 | 종합 평가 |
|------|--------|-----------|
| 1위 | **Clad** | S (유일한 힐러, 역할 완벽) |
---
## 2. 밸런스 이슈 분석
### 2.1. 긴급 조정 필요 (우선순위 최고)
#### ⚠️ Rio - 과도한 DPS
**문제점**:
- 지속 DPS 268 (2위 Cazimord 221보다 +47, +21% 과다)
- 평타 DPS 196 (전체 1위)
- 짧은 쿨타임 (연속 찌르기 2.6초, 접근 3초)
- 유틸리티도 중위권 (13점)
**원인 분석**:
1. **초짧은 쿨타임**: 왜곡 룬 적용 시 2.6~5.25초
2. **높은 평타 비중**: 30초 중 18초 평타 (60%)
3. **스킬 배율**: 1.0배로 낮지 않음
4. **Chain Score**: 추가 피해 시너지
**영향**:
- 다른 DPS 역할 (Cazimord, Lian, Nave) 경쟁력 하락
- 파티에서 Rio 픽률 과다 예상
- 역할 다양성 감소
**밸런스 영향도**: ⭐⭐⭐⭐⭐ (최고)
---
#### ⚠️ Urud - 과도하게 낮은 DPS
**문제점**:
- 지속 DPS 82 (9위, 서포터 Clad 76과 비슷)
- 평타 DPS 90 → Reload 고려 시 실제 ~60
- 스킬 배율 낮음 (다발 화살 1.2배, 독침 화살 0.8배)
**원인 분석**:
1. **Reload 페널티**: 6발 발사 후 2초 재장전
2. **낮은 스킬 배율**: 다발 화살 1.2배 (Lian 속사 0.85×4 = 3.4배와 비교)
3. **긴 쿨타임**: 7초 (Lian 5.25초 왜곡 룬)
**영향**:
- 원거리 역할에서 Lian에게 완전히 밀림 (Lian 219 vs Urud 82 = 2.7배)
- CC는 좋지만 DPS가 너무 낮아 채택률 저조 예상
- "원거리 딜러"라는 역할 정체성 혼란
**밸런스 영향도**: ⭐⭐⭐⭐⭐ (최고)
---
### 2.2. 개선 검토 필요 (우선순위 중)
#### 🔶 Baran - "파워 전사"인데 낮은 DPS
**문제점**:
- 지속 DPS 128 (7위, 전사 중 2위)
- STR 25, CON 25로 최고 스탯인데 DPS 중하위
- "파워 전사" 컨셉과 불일치
**원인 분석**:
1. **갈고리 배율 너무 낮음**: 0.25배 (사실상 유틸리티 스킬)
2. **쿨타임 김**: 갈고리 13초 (왜곡 룬 9.75초)
3. **궁극기 배율**: 1.7배는 높지만 버스트 DPS 184로 중위권
**영향**:
- Baran의 역할 정체성 애매 (CC 특화? 파워 전사?)
- Stun 3초 궁극기는 유용하지만, 지속 DPS가 낮아 채택률 중간
- Cazimord(221)와 격차 너무 큼
**밸런스 영향도**: ⭐⭐⭐ (중)
---
### 2.3. 양호한 밸런스
다음 스토커들은 밸런스가 양호합니다:
| 스토커 | 밸런스 상태 | 이유 |
|--------|-------------|------|
| **Cazimord** | ✅ 우수 | 고DPS(221)를 고난이도(Parrying)로 보상, 버스트 1위 |
| **Lian** | ✅ 양호 | 고DPS(219) + 낮은 유틸리티 균형 |
| **Nave** | ✅ 양호 | 고DPS(202) + 광역 버스트(241) 상황적 우위 |
| **Sinobu** | ✅ 양호 | 중DPS(176) + 고유틸(16점) 균형 |
| **Rene** | ✅ 우수 | 중DPS(148) + 최고유틸(18점) 서포터 역할 |
| **Hilda** | ✅ 우수 | 중DPS(117) + 고유틸(16점) 탱커 역할 |
| **Clad** | ✅ 우수 | 저DPS(76) + 최고유틸(18점) 힐러 역할 |
---
## 3. 구체적 개선안
### 3.1. Rio 너프 (최우선)
#### 목표
- 현재: DPS 268
- 목표: DPS 230~240 (Cazimord와 비슷한 수준)
- 감소량: 약 28~38 (-10~15%)
#### 개선안 A: 스킬 쿨타임 증가
**변경 내용**:
```
연속 찌르기: 3.5초 → 4.0초 (+0.5초)
접근: 4초 → 4.5초 (+0.5초)
단검 투척: 7초 → 8초 (+1초)
```
**예상 효과**:
- 30초간 스킬 사용 횟수 감소
- 연속 찌르기: 10회 → 9회
- 접근: 9회 → 8회
- 평타 필러 시간 약간 증가
- **예상 DPS**: ~240 (-28, -10%)
**장점**: ✅ 스킬 배율 유지, 플레이 패턴 유지
**단점**: ⚠️ 왜곡 룬 효과 감소 시 더 조정 필요할 수 있음
---
#### 개선안 B: 스킬 배율 감소
**변경 내용**:
```
연속 찌르기: 1.0배 → 0.9배 (-10%)
접근: 1.0배 → 0.9배 (-10%)
단검 투척: 1.0배 → 0.9배 (-10%)
```
**예상 효과**:
- 스킬 피해 10% 감소
- 30초 로테이션:
- 연속 찌르기: 2,520 → 2,268
- 접근: 1,361 → 1,225
- 단검 투척: 630 → 567
- **예상 DPS**: ~235 (-33, -12%)
**장점**: ✅ 직관적, 명확한 효과
**단점**: ⚠️ 모든 스킬 배율 동일(1.0→0.9)하여 단조로움
---
#### 개선안 C: 평타 배율 감소 (추천 ⭐)
**변경 내용**:
```
평타 콤보 배율: 0.8 + 0.8 + 1.2 = 2.8배 → 0.7 + 0.7 + 1.0 = 2.4배 (-14%)
```
**예상 효과**:
- 평타 DPS: 196 → 168 (-14%)
- 30초 로테이션에서 평타 필러 18초 비중
- 평타 피해: 3,528 → 3,024
- **예상 DPS**: ~238 (-30, -11%)
**장점**:
- ✅ Rio의 핵심인 "빠른 평타"는 유지
- ✅ 스킬 회전율 유지 (플레이 패턴 변화 최소)
- ✅ Chain Score 시스템 영향 없음
**단점**: ⚠️ 평타만 너프하여 스킬 중심 플레이로 유도될 수 있음
---
#### 권장 방안: **개선안 C (평타 배율 감소)**
**이유**:
1. Rio의 정체성(빠른 평타 암살자) 유지
2. 스킬 로테이션 변화 없음
3. 직관적 조정 (평타 배율만 조정)
4. 목표 DPS (230~240) 달성
---
### 3.2. Urud 버프 (최우선)
#### 목표
- 현재: DPS 82
- 목표: DPS 100~120
- 증가량: 약 18~38 (+22~46%)
#### 개선안 A: Reload 시간 단축 (추천 ⭐)
**변경 내용**:
```
Reload 시간: 2초 → 1.5초 (-0.5초)
```
**예상 효과**:
- 평타 DPS (Reload 포함): 90 → 105 (+17%)
- 30초 로테이션:
- Reload 횟수: 4회
- Reload 총 시간: 8초 → 6초 (-2초)
- 평타 필러 시간: 14초 → 16초 (+2초)
- 평타 피해: 1,260 → 1,680 (+420)
- **예상 DPS**: ~96 (+14, +17%)
**추가 조정 필요**: 목표 100~120에 약간 미달, 스킬 배율 추가 증가 검토
**장점**:
- ✅ Lian과 공유하는 Reload 시스템 개선 (Lian도 수혜)
- ✅ 직관적, QoL 개선
- ✅ 평타 DPS 향상
**단점**: ⚠️ 단독으로는 목표 DPS 미달
---
#### 개선안 B: 다발 화살 배율 증가
**변경 내용**:
```
다발 화살: 1.2배 → 1.5배 (+25%)
```
**예상 효과**:
- 30초간 다발 화살 4회
- 피해: 576 → 720 (+144)
- **예상 DPS**: ~87 (+5, +6%)
**장점**: ✅ 주력 스킬 강화
**단점**: ⚠️ 효과 미미, 목표 DPS 미달
---
#### 개선안 C: 복합 버프 (추천 ⭐⭐)
**변경 내용**:
```
1. Reload 시간: 2초 → 1.5초
2. 다발 화살: 1.2배 → 1.6배 (+33%)
3. 독침 화살: 0.8배 → 1.0배 (+25%)
```
**예상 효과**:
- Reload 단축: +420 평타 피해
- 다발 화살: 576 → 768 (+192)
- 독침 화살: 384 → 480 (+96)
- DOT 유지 (240)
- **예상 DPS**: ~109 (+27, +33%)
**장점**:
- ✅ 목표 DPS (100~120) 달성
- ✅ 주력 스킬 모두 강화
- ✅ Reload QoL 개선
**단점**: ⚠️ 여러 조정 필요 (복잡도 증가)
---
#### 권장 방안: **개선안 C (복합 버프)**
**이유**:
1. 목표 DPS 범위 달성 (~109)
2. Reload QoL 개선 (Lian도 수혜)
3. 주력 스킬 강화로 플레이 만족도 증가
4. 원거리 딜러로서 역할 정립
---
### 3.3. Baran 개선 (검토 단계)
#### 목표
- 현재: DPS 128
- 목표: DPS 140~150 (전사 중 2위 유지, Hilda와 격차 확보)
- 증가량: 약 12~22 (+9~17%)
#### 개선안 A: 갈고리 배율 증가
**변경 내용**:
```
갈고리 투척: 0.25배 → 0.5배 (2배 증가)
```
**예상 효과**:
- 30초간 갈고리 3회
- 피해: 95 → 189 (+94)
- **예상 DPS**: ~131 (+3, +2%)
**장점**: ✅ "갈고리로 끌어당기는" 플레이 스타일 강화
**단점**: ⚠️ 효과 미미 (쿨타임이 길어 사용 횟수 적음)
---
#### 개선안 B: 후려치기 배율 증가 (추천 ⭐)
**변경 내용**:
```
후려치기: 1.2배 → 1.4배 (+17%)
```
**예상 효과**:
- 30초간 후려치기 5회
- 피해: 756 → 882 (+126)
- **예상 DPS**: ~132 (+4, +3%)
**단점**: ⚠️ 효과 미미
---
#### 개선안 C: 복합 버프 (추천 ⭐⭐)
**변경 내용**:
```
1. 갈고리 투척: 0.25배 → 0.5배
2. 후려치기: 1.2배 → 1.4배
3. 깊게 찌르기: 1.1배 → 1.3배
```
**예상 효과**:
- 갈고리: +94
- 후려치기: +126
- 깊게 찌르기: +126
- **예상 DPS**: ~139 (+11, +9%)
**장점**: ✅ 목표 DPS 범위 달성
**단점**: ⚠️ 여러 조정 필요
---
#### 권장 방안: **개선 보류, 현상 유지**
**이유**:
1. Baran은 CC 특화 역할로 차별화 충분
2. Stun 3초 궁극기로 유틸리티 높음
3. DPS 128은 "파워 전사"보다는 "CC 전사"로 재정의 가능
4. 밸런스 영향도 중간 (Rio, Urud보다 낮음)
**대안**:
- 컨셉 재정의: "파워 전사" → "CC 특화 전사"
- 스킬 설명 수정으로 기대치 조정
---
## 4. 조정 우선순위
### 4.1. Phase 1: 긴급 조정 (즉시)
| 우선순위 | 스토커 | 조정 방향 | 조정 내용 | 목표 DPS |
|----------|--------|-----------|-----------|----------|
| **1순위** | **Rio** | 너프 | 평타 배율 감소 (2.8 → 2.4) | 238 (-30) |
| **2순위** | **Urud** | 버프 | Reload 1.5초, 다발 1.6배, 독침 1.0배 | 109 (+27) |
**예상 효과**:
- Rio DPS: 268 → 238 (2위와 격차 21% → 8%)
- Urud DPS: 82 → 109 (Clad와 격차 확보)
- DPS 순위 변화:
- 1위: Rio (238) - 여전히 1위지만 격차 감소
- 2위: Cazimord (221)
- 3위: Lian (219)
---
### 4.2. Phase 2: 검토 및 모니터링 (1개월 후)
Phase 1 조정 후 데이터 수집 및 분석
**모니터링 항목**:
1. Rio 채택률 변화
2. Urud 채택률 변화
3. Baran vs Hilda 채택률 비교
4. 파티 구성 다양성
**추가 조정 검토**:
- Baran 버프 여부 (DPS 128 → 140)
- Lian Reload 단축 수혜로 DPS 상승 여부 확인
---
### 4.3. Phase 3: 장기 밸런스 (3개월 후)
**검토 사항**:
1. **Nave vs Cazimord 버스트 밸런스**
- 둘 다 10.0배 궁극기
- 상황별 우위 명확한지 확인
2. **Lian 궁극기 강화 필요 여부**
- Reload 단축 수혜로 DPS 상승 시 평가
3. **Sinobu vs Rio 암살자 균형**
- Rio 너프 후 Sinobu 채택률 변화
4. **Rene vs Clad 서포터 역할**
- 파티 구성에서 선호도 분석
---
## 5. 밸런스 철학 및 가이드라인
### 5.1. 역할별 DPS 목표 범위
조정 후 목표 DPS 범위 (지속 DPS 기준):
| 역할 | 목표 DPS 범위 | 이유 |
|------|---------------|------|
| **암살자** | 200~240 | 고DPS 역할, 생존력/유틸 낮음 |
| **전사** | 120~220 | 역할 다양 (탱커 100대, DPS 200대) |
| **마법사** | 150~200 | 광역 특화, 버스트 강함 |
| **원거리** | 100~220 | 안전한 후방, 지속 딜 |
| **서포터** | 70~80 | 힐러 역할, DPS 최하위 |
### 5.2. DPS vs 유틸리티 트레이드오프
**원칙**:
- 고DPS → 낮은 유틸리티
- 저DPS → 높은 유틸리티
**목표 균형**:
```
DPS + (유틸리티 × 15) = 270~290
```
**현재 상태**:
| 스토커 | DPS | 유틸 | 총점 | 상태 |
|--------|-----|------|------|------|
| Rio | 268 | 13 | 463 | ⚠️ 과다 |
| Cazimord | 221 | 15 | 446 | ✅ 양호 |
| Lian | 219 | 14 | 429 | ✅ 양호 |
| Urud | 82 | 14 | 292 | ⚠️ 부족 |
| Clad | 76 | 18 | 346 | ✅ 양호 |
**조정 후 예상**:
| 스토커 | DPS | 유틸 | 총점 | 상태 |
|--------|-----|------|------|------|
| Rio | 238 | 13 | 433 | ✅ 개선 |
| Urud | 109 | 14 | 319 | ✅ 개선 |
---
### 5.3. 궁극기 밸런스 가이드라인
**원칙**:
1. **피해형 궁극기**: 버스트 DPS 180~260
2. **버프형 궁극기**: 15초 이상 지속, 팀 기여
3. **방어형 궁극기**: 위기 탈출, 생존력 극대화
**현재 상태**: ✅ 양호
- Cazimord (256), Nave (241): 피해형 최상위
- Hilda, Urud, Rio, Lian, Rene: 버프형 15초 이상
- Sinobu, Clad: 방어형 강력
---
### 5.4. 특수 시스템 밸런스
**원칙**:
- 고난이도 시스템 → 고보상 (Parrying, Chain Score)
- 페널티 시스템 → DPS 보정 필요 (Reload)
**현재 상태**:
- ✅ Parrying (Cazimord): 0% vs 100% = 166 vs 221 (+33%) - 적절
- ✅ Chain Score (Rio): 추가 피해 - **과다 (너프 필요)**
- ⚠️ Reload (Urud, Lian): 페널티 과다 - **버프 필요**
**조정 후**:
- Reload 1.5초로 페널티 완화
- Rio 평타 너프로 Chain Score 시너지 감소
---
## 6. 예상 티어 변화 (조정 후)
### 6.1. 조정 후 종합 티어
| 티어 | 스토커 | 지속 DPS | 유틸리티 | 변화 |
|------|--------|----------|----------|------|
| **S+** | **Cazimord** | 221 | 15점 | 변화 없음 (모델 케이스) |
| | **Rio** | **238** | 13점 | OP → S+ (⬇️ 너프) |
| | **Rene** | 148 | 18점 | 변화 없음 |
| **S** | **Clad** | 76 | 18점 | 변화 없음 |
| | **Lian** | 219 | 14점 | 변화 없음 |
| | **Nave** | 202 | 13점 | 변화 없음 |
| | **Hilda** | 117 | 16점 | 변화 없음 |
| **A** | **Sinobu** | 176 | 16점 | 변화 없음 |
| | **Baran** | 128 | 14점 | 변화 없음 |
| | **Urud** | **109** | 14점 | B → A (⬆️ 버프) |
**변화 요약**:
- Rio: OP → S+ (여전히 최상위, 격차 감소)
- Urud: B → A (경쟁력 확보)
---
### 6.2. 조정 후 DPS 순위
| 순위 | 스토커 | DPS | 변화 |
|------|--------|-----|------|
| 1 | Rio | 238 | ⬇️ -30 |
| 2 | Cazimord | 221 | - |
| 3 | Lian | 219 | - |
| 4 | Nave | 202 | - |
| 5 | Sinobu | 176 | - |
| 6 | Rene | 148 | - |
| 7 | Baran | 128 | - |
| 8 | Hilda | 117 | - |
| 9 | **Urud** | **109** | ⬆️ +27 |
| 10 | Clad | 76 | - |
**DPS 격차**:
- 1위 vs 2위: 238 vs 221 = **+8%** (개선 전 +21%)
- 서포터 제외 최대 격차: 238 vs 109 = **2.2배** (개선 전 3.3배)
---
## 7. 최종 권장 사항
### 7.1. 즉시 적용 (Phase 1)
**Rio 너프**:
```
평타 콤보 배율: 0.8 + 0.8 + 1.2 = 2.8배
→ 0.7 + 0.7 + 1.0 = 2.4배
```
- 예상 DPS: 268 → 238
- 적용 난이도: ⭐⭐ (평타 배율만 조정)
**Urud 버프**:
```
Reload 시간: 2초 → 1.5초
다발 화살: 1.2배 → 1.6배
독침 화살: 0.8배 → 1.0배
```
- 예상 DPS: 82 → 109
- 적용 난이도: ⭐⭐⭐ (3가지 조정)
- **참고**: Reload 단축은 Lian도 수혜 (DPS 219 → ~225 예상)
---
### 7.2. 모니터링 (Phase 2 - 1개월 후)
**데이터 수집**:
1. Rio, Urud 채택률 변화
2. 파티 구성 다양성
3. 플레이어 피드백
**추가 조정 검토**:
- Baran DPS 버프 필요 여부
- Lian Reload 수혜로 추가 조정 필요 여부
---
### 7.3. 장기 검토 (Phase 3 - 3개월 후)
**밸런스 재평가**:
1. 전체 스토커 채택률 분석
2. 역할별 다양성 확보 여부
3. 궁극기 밸런스 (Nave vs Cazimord)
---
## 8. 결론
### 8.1. 현재 밸런스 요약
**강점** ✅:
- 역할 다양성 우수 (5개 역할군)
- 각 역할 내 차별화 명확
- 유틸리티 트레이드오프 대부분 양호
- 궁극기 다양성 우수
**약점** ⚠️:
- Rio DPS 과다 (268, 2위보다 +21%)
- Urud DPS 부족 (82, 원거리 역할 위협)
- 일부 역할 격차 과다 (Lian vs Urud = 2.7배)
---
### 8.2. 조정 후 기대 효과
**밸런스 개선**:
- DPS 격차 감소: 1위 vs 2위 +21% → +8%
- 역할군 내 격차 감소: 원거리 2.7배 → 2.0배
- 채택률 다양성 증가 예상
**플레이어 경험**:
- Rio: 여전히 강력하지만 압도적이지 않음
- Urud: 원거리 딜러로서 경쟁력 확보
- 전체: 더 다양한 파티 구성 가능
---
### 8.3. 향후 밸런스 방향
**원칙**:
1. **역할 정체성 유지**: 각 스토커의 고유 플레이스타일 보존
2. **점진적 조정**: 급격한 변화 지양, 단계적 개선
3. **데이터 기반**: 플레이어 피드백 및 통계 기반 조정
4. **다양성 추구**: 여러 스토커가 경쟁력 있도록
**비전**:
- 모든 스토커가 상황에 따라 채택 가능
- 파티 구성의 다양성 극대화
- 플레이어 선호도 존중 (강제 메타 지양)
---
**생성 일시**: 2025-10-24 02:00
**분석 기준**: 레벨 20, 기어스코어 400, 최적 플레이
**데이터 소스**: 03~07번 문서 종합

View File

@ -0,0 +1,360 @@
# 던전 스토커즈 전투 밸런스 분석
## 📋 프로젝트 개요
**던전 스토커즈 (DungeonStalkers)** 10명 스토커에 대한 종합적인 전투 밸런스 분석 문서입니다.
- **분석 일시**: 2025-10-24
- **분석 대상**: Hilda, Urud, Nave, Baran, Rio, Clad, Rene, Sinobu, Lian, Cazimord (10명)
- **분석 기준**: 레벨 20, 기어스코어 400, 최적 플레이
- **엔진**: Unreal Engine 5.5.4
- **시스템**: Gameplay Ability System (GAS)
---
## 🎯 핵심 발견사항
### ⚠️ 긴급 조정 필요
| 스토커 | 문제점 | 개선안 | 예상 효과 |
|--------|--------|--------|-----------|
| **Rio** | DPS 268 (2위보다 +21% 과다) | 평타 배율 2.8→2.4 | DPS 238 (-11%) |
| **Urud** | DPS 82 (과도하게 낮음) | Reload 1.5초 + 스킬 배율 증가 | DPS 109 (+33%) |
### 📊 DPS 순위 (조정 전)
1. **Rio** - 268 (암살자) ⚠️
2. **Cazimord** - 221 (전사)
3. **Lian** - 219 (원거리)
4. **Nave** - 202 (마법사)
5. **Sinobu** - 176 (암살자)
6. **Rene** - 148 (마법사)
7. **Baran** - 128 (전사)
8. **Hilda** - 117 (전사)
9. **Urud** - 82 (원거리) ⚠️
10. **Clad** - 76 (서포터)
### 🏆 버스트 DPS 순위
1. **Cazimord** - 256 (칼날폭풍 10.0배, 단일 대상)
2. **Nave** - 241 (해방 10.0배, 관통 광역)
3. **Rio** - 200 (민감)
4. **Sinobu** - 196 (반환)
5. **Baran** - 184 (일격분쇄 + Stun 3초)
---
## 📚 문서 구조
### 주요 문서 (읽기 권장 순서)
| 문서 | 설명 | 중요도 |
|------|------|--------|
| **[01_요약.md](./01_요약.md)** | 전체 분석 요약 및 핵심 결론 | ⭐⭐⭐⭐⭐ |
| **[08_밸런스_티어_및_개선안.md](./08_밸런스_티어_및_개선안.md)** | 최종 티어표 및 구체적 조정안 | ⭐⭐⭐⭐⭐ |
| **[04_DPS_계산_결과.md](./04_DPS_계산_결과.md)** | 평타/스킬/버스트 DPS 상세 계산 | ⭐⭐⭐⭐ |
| **[07_역할별_차별화.md](./07_역할별_차별화.md)** | 5개 역할군 상세 비교 | ⭐⭐⭐⭐ |
| **[06_유틸리티_평가.md](./06_유틸리티_평가.md)** | CC/생존/기동/팀기여 평가 | ⭐⭐⭐ |
| **[03_스토커별_기본데이터.md](./03_스토커별_기본데이터.md)** | 10명 스킬 상세 정보 | ⭐⭐⭐ |
| **[05_카지모르드_밸런스_검증.md](./05_카지모르드_밸런스_검증.md)** | Cazimord Parrying 시스템 분석 | ⭐⭐ |
| **[02_분석_전제조건.md](./02_분석_전제조건.md)** | 레벨, 장비, 룬 전제 | ⭐⭐ |
### 빠른 탐색
**DPS 정보가 필요하다면?**
→ [04_DPS_계산_결과.md](./04_DPS_계산_결과.md) - 평타/스킬/버스트 DPS 모두 포함
**특정 스토커 정보가 필요하다면?**
→ [03_스토커별_기본데이터.md](./03_스토커별_기본데이터.md) - 10명 상세 스킬 정보
**밸런스 조정안이 필요하다면?**
→ [08_밸런스_티어_및_개선안.md](./08_밸런스_티어_및_개선안.md) - 구체적 수치 포함
**역할별 비교가 필요하다면?**
→ [07_역할별_차별화.md](./07_역할별_차별화.md) - 전사/원거리/마법사/암살자/서포터
---
## 🔍 주요 분석 내용
### 1. DPS 분석
**3가지 DPS 지표**:
- **평타 DPS**: 평타 콤보만 사용 시
- **지속 DPS**: 30초 스킬 로테이션 (실전)
- **버스트 DPS**: 10초 궁극기 풀콤보 (최대 화력)
**BaseDamage 계산**:
```
물리 딜러: (STR or DEX + 80) × 1.20
마법 딜러: (INT + 80) × 1.10
서포터: (주스탯 + 80) × 1.00
```
**주요 발견**:
- Rio 지속 DPS 268 (압도적 1위, 2위보다 +21%)
- Cazimord 버스트 DPS 256 (패링 100% 시)
- Nave 버스트 DPS 241 (광역 관통)
---
### 2. 유틸리티 평가
**5가지 평가 항목**:
1. **CC**: Stun, Snare, Knockback, 경직
2. **생존력**: Blocking, Parrying, 힐, Lifesteal
3. **기동성**: 돌진, 대시, 텔레포트
4. **팀 기여**: 버프, 디버프, 힐, 보호막
5. **궁극기**: 공격/버프/방어형 유틸리티
**최고 유틸리티**:
- Clad (18점): 힐 + DOT 제거 + 보호막 300
- Rene (18점): Lifesteal + 파티 흡혈 20초
- Hilda (16점): Blocking 100% + 도발
- Sinobu (16점): 기동성 + 궁극기 반사+막기
---
### 3. 역할별 차별화
**전사 (3명)**:
- Hilda: 탱커 (Blocking, 도발)
- Baran: CC 특화 (Stun 3초 궁극기)
- Cazimord: 고DPS (Parrying, 평타 중심)
**원거리 (2명)**:
- Urud: CC 특화 (덫 Snare) ⚠️ DPS 부족
- Lian: 고화력 (속사 4발, 만충전 1.5배)
**마법사 (2명)**:
- Nave: 광역 폭딜 (궁극기 관통 10.0배)
- Rene: 소환사 서포터 (정령, 파티 흡혈)
**암살자 (2명)**:
- Rio: DPS 특화 ⚠️ 과다
- Sinobu: 기동성 특화
**서포터 (1명)**:
- Clad: 유일한 힐러
---
### 4. 밸런스 티어
#### 종합 티어 (DPS + 유틸리티)
| 티어 | 스토커 | 평가 |
|------|--------|------|
| **OP** | Rio | 과도한 DPS, 너프 필요 |
| **S+** | Cazimord, Rene | 역할 모델 |
| **S** | Clad, Lian, Nave, Hilda | 상위, 경쟁력 우수 |
| **A** | Sinobu, Baran | 중상위, 밸런스 양호 |
| **B** | Urud | 중하위, 버프 필요 |
---
## 🛠️ 개선안 (구체적 수치)
### Rio 너프 (최우선)
**현재 문제**:
- 지속 DPS 268 (2위 Cazimord 221보다 +47, +21%)
- 평타 DPS 196 (전체 1위)
- 짧은 쿨타임 (연속 찌르기 2.6초, 접근 3초)
**권장 조정**:
```
평타 배율: 0.8 + 0.8 + 1.2 = 2.8배
→ 0.7 + 0.7 + 1.0 = 2.4배 (-14%)
```
**예상 효과**:
- 평타 DPS: 196 → 168
- 지속 DPS: 268 → **238** (-30, -11%)
- 1위 vs 2위 격차: +21% → **+8%**
**장점**:
- ✅ 빠른 평타 정체성 유지
- ✅ 스킬 로테이션 변화 없음
- ✅ 목표 DPS 달성
---
### Urud 버프 (최우선)
**현재 문제**:
- 지속 DPS 82 (서포터 Clad 76과 비슷)
- 원거리 역할 정체성 위협
- Reload 페널티 + 낮은 스킬 배율
**권장 조정**:
```
1. Reload 시간: 2초 → 1.5초 (-0.5초)
2. 다발 화살: 1.2배 → 1.6배 (+33%)
3. 독침 화살: 0.8배 → 1.0배 (+25%)
```
**예상 효과**:
- Reload 단축: +420 평타 피해
- 스킬 강화: +288 피해
- 지속 DPS: 82 → **109** (+27, +33%)
**장점**:
- ✅ 원거리 딜러 경쟁력 확보
- ✅ Reload QoL 개선 (Lian도 수혜)
- ✅ 주력 스킬 강화
**참고**: Lian도 Reload 단축 수혜 (DPS 219 → ~225 예상)
---
### Baran 검토 (보류)
**현재 상태**:
- 지속 DPS 128 ("파워 전사"인데 중하위)
- Stun 3초 궁극기로 CC 특화
**권장 사항**:
- 현상 유지
- 컨셉 재정의: "파워 전사" → "CC 특화 전사"
- Stun 3초 궁극기로 차별화 충분
---
## 📊 조정 우선순위
### Phase 1: 즉시 적용
1. **Rio 평타 배율 너프**: 2.8 → 2.4
2. **Urud 복합 버프**: Reload + 스킬 배율
**목표**: DPS 격차 완화 (1위 vs 2위 +21% → +8%)
---
### Phase 2: 모니터링 (1개월 후)
**데이터 수집**:
- Rio, Urud 채택률 변화
- 파티 구성 다양성
- 플레이어 피드백
**추가 검토**:
- Baran 버프 필요 여부
- Lian Reload 수혜로 추가 조정 여부
---
### Phase 3: 장기 검토 (3개월 후)
**재평가 항목**:
- 전체 밸런스
- 궁극기 밸런스 (Nave vs Cazimord)
- 신규 스토커 추가 시 기준
---
## 📖 데이터 소스
분석에 사용된 Unreal Engine 데이터:
- **DT_CharacterStat**: 기본 스탯, 스킬 ID 목록
- **DT_CharacterAbility**: 평타 몽타주, 타이밍
- **DT_Skill**: 스킬 상세 정보 (배율, 쿨타임, 마나, 속성)
- **GameplayEffect Blueprints**: 궁극기 효과 (GE_Skill_Ultimate_*)
**추출 방법**:
1. UAssetGUI로 .uasset 파일 열기
2. JSON 변환 (FModel 사용)
3. Python 스크립트로 데이터 추출 및 검증
---
## 💡 밸런스 철학
### 목표
1. **역할 다양성**: 모든 스토커가 상황에 따라 경쟁력 확보
2. **역할 정체성**: 각 스토커의 고유 플레이스타일 보존
3. **고숙련 보상**: Parrying, Chain Score 등 유지
4. **페널티 완화**: Reload 등 불편 요소 개선
### 원칙
**DPS vs 유틸리티 균형**:
```
DPS + (유틸리티 × 15) = 270~290
```
**역할별 DPS 범위**:
- 암살자: 200~240
- 전사: 120~220 (역할별 다양)
- 마법사: 150~200
- 원거리: 100~220
- 서포터: 70~80
**궁극기 밸런스**:
- 피해형: 버스트 DPS 180~260
- 버프형: 15초 이상 지속
- 방어형: 생존력 극대화
---
## ✅ 검증 및 품질 보증
### 데이터 검증
- ✅ DT_Skill에서 직접 추출 (수동 입력 오류 방지)
- ✅ Nave 궁극기 Tick/Count 필드 확인 (10.0배 총 배율)
- ✅ Cazimord Parrying 쿨타임 감소 수치 검증
- ✅ 모든 스킬명 DT_Skill 기준 정확히 매칭
### 계산 검증
- ✅ BaseDamage 계산식 룬 효과 반영
- ✅ 30초 로테이션 스킬 사용 횟수 정확히 계산
- ✅ 버스트 DPS 10초 풀콤보 정확히 재현
- ✅ 왜곡 룬 (-25% 쿨타임) 반영
### 문서 일관성
- ✅ 03~08번 문서 DPS 수치 일치
- ✅ 스킬명 통일 (DT_Skill RowName 기준)
- ✅ 역할 분류 일관 (전사/원거리/마법사/암살자/서포터)
---
## 📝 버전 히스토리
### v1.0 (2025-10-24)
**주요 변경**:
- Nave 궁극기 수정 (1.0배 → 10.0배 총 배율)
- 모든 스킬명 DT_Skill 기준으로 정정
- Rio DPS 과다 발견 및 너프안 제시
- Urud DPS 부족 발견 및 버프안 제시
**문서 완성**:
- 01~08번 전체 문서 작성 완료
- README 작성 완료
---
## 🔗 관련 링크
- **프로젝트**: DungeonStalkers (Unreal Engine 5.5.4)
- **분석 도구**: UAssetGUI, FModel, Python
- **시스템**: Gameplay Ability System (GAS)
---
## 📧 문의
분석 관련 문의 또는 추가 데이터 요청은 이슈로 등록해주세요.
---
**분석자**: Claude (Anthropic)
**생성 일시**: 2025-10-24
**최종 업데이트**: 2025-10-24 02:30
**라이선스**: 내부 사용 전용

View File

@ -0,0 +1,196 @@
# DPS 시나리오 비교 분석 v2
**생성 시각**: 2025-10-27 17:46:37
---
## 📋 분석 전제 조건
### 기본 설정
- **레벨**: 20
- **기어 스코어**: 400
- **플레이 스타일**: 최적 플레이
- **쿨타임 감소**: 25.0% (왜곡 룬)
### BaseDamage 계산식
```python
# 물리 딜러 (STR/DEX)
Physical_BaseDamage = (주스탯 + 80) × 1.20
# 마법 딜러 (INT)
Magical_BaseDamage = (INT + 80) × 1.10
# 탱커/서포터
Support_BaseDamage = (주스탯 + 80) × 1.00
```
---
## 🗡️ 시나리오 1: 평타 DPS
**목적**: 순수 평타만으로 지속 딜 측정
| 순위 | 스토커 | BaseDamage | 평타 DPS | 콤보 시간 | 평타 배율 합계 |
|------|--------|------------|----------|-----------|----------------|
| 1 | 시노부 | 126.0 | 88.94 | 2.27초 | 1.6 |
| 2 | 힐다 | 120.0 | 78.83 | 4.57초 | 3.0 |
| 3 | 리오 | 126.0 | 76.58 | 3.87초 | 2.35 |
| 4 | 바란 | 126.0 | 72.43 | 5.57초 | 3.2 |
| 5 | 네이브 | 115.5 | 70.0 | 3.3초 | 2.0 |
| 6 | 카지모르드 | 126.0 | 69.57 | 5.43초 | 3.0 |
| 7 | 레네 | 110.0 | 55.93 | 5.9초 | 3.0 |
| 8 | 클라드 | 95.0 | 47.88 | 4.17초 | 2.1 |
| 9 | 리안 | 120.0 | 36.73 | 3.27초 | 1.0 |
| 10 | 우르드 | 120.0 | 36.55 | 3.28초 | 1.0 |
---
## ⚔️ 시나리오 2: 스킬 로테이션 DPS (30초)
**목적**: 스킬 + 평타 조합한 실전 DPS
| 순위 | 스토커 | 로테이션 DPS | 스킬 피해 | 평타 피해 | 평타 사용 시간 |
|------|--------|--------------|-----------|-----------|----------------|
| 1 | 시노부 | 80.94 | 2268.0 | 160.09 | 1.8초 |
| 2 | 우르드 | 74.26 | 2184.0 | 43.86 | 1.2초 |
| 3 | 바란 | 71.31 | 611.1 | 1528.27 | 21.1초 |
| 4 | 리오 | 68.13 | 2028.6 | 15.32 | 0.2초 |
| 5 | 힐다 | 67.3 | 600.0 | 1418.94 | 18.0초 |
| 6 | 클라드 | 60.1 | 855.0 | 948.02 | 19.8초 |
| 7 | 네이브 | 50.27 | 381.15 | 1127.0 | 16.1초 |
| 8 | 카지모르드 | 36.2 | 327.6 | 758.31 | 10.9초 |
| 9 | 레네 | 30.44 | 907.5 | 5.59 | 0.1초 |
| 10 | 리안 | 16.46 | 336.0 | 157.94 | 4.3초 |
---
## 💥 시나리오 3: 버스트 DPS (10초)
**목적**: 궁극기 포함 최대 화력
| 순위 | 스토커 | 버스트 DPS | 궁극기 | 궁극기 피해 | 스킬 피해 | 평타 피해 |
|------|--------|------------|--------|-------------|-----------|----------|
| 1 | 바란 | 65.93 | ✅ | 0 | 321.3 | 338.04 |
| 2 | 시노부 | 55.82 | ✅ | 0.0 | 453.6 | 104.61 |
| 3 | 클라드 | 53.99 | ❌ | 0 | 142.5 | 397.4 |
| 4 | 리안 | 51.65 | ❌ | 0 | 426.0 | 90.49 |
| 5 | 우르드 | 51.08 | ✅ | 120.0 | 312.0 | 78.79 |
| 6 | 리오 | 46.86 | ✅ | 37.8 | 378.0 | 52.82 |
| 7 | 힐다 | 46.13 | ✅ | 60.0 | 252.0 | 149.34 |
| 8 | 레네 | 34.69 | ❌ | 0 | 242.0 | 104.87 |
| 9 | 네이브 | 33.66 | ✅ | 115.5 | 57.75 | 163.33 |
| 10 | 카지모르드 | 23.14 | ✅ | 100.8 | 126.0 | 4.64 |
---
## 🔥 특수 상황 분석
### 1. DoT 스킬 DPS (대상 HP별)
#### 독성 화살 (우르드)
- **DoT 타입**: Poison
- **설명**: 대상 MaxHP의 20% (5초간)
| 대상 HP | DoT DPS |
|---------|----------|
| 100HP | 4.0 |
| 500HP | 20.0 |
| 1000HP | 40.0 |
#### 독기 화살 (레네)
- **DoT 타입**: Bleed
- **설명**: 고정 20 피해 (5초간)
| 대상 HP | DoT DPS |
|---------|----------|
| 100HP | 4.0 |
| 500HP | 4.0 |
| 1000HP | 4.0 |
#### 작열 (카지모르드)
- **DoT 타입**: Burn
- **설명**: 대상 MaxHP의 10% (3초간)
| 대상 HP | DoT DPS |
|---------|----------|
| 100HP | 3.33 |
| 500HP | 16.67 |
| 1000HP | 33.33 |
#### 정령 소환: 화염 (레네)
- **DoT 타입**: Burn
- **설명**: 대상 MaxHP의 10% (3초간)
| 대상 HP | DoT DPS |
|---------|----------|
| 100HP | 3.33 |
| 500HP | 16.67 |
| 1000HP | 33.33 |
### 2. 소환체 독립 DPS
#### 정령 소환: 화염
- **소환체**: Ifrit
- **지속 시간**: 20초
- **공격 사이클**: 1.46초
- **총 공격 횟수**: 13.71회
- **독립 DPS**: 90.51
- **비고**: 1개 몽타주 순차 루프
#### 정령 소환: 냉기
- **소환체**: Shiva
- **지속 시간**: 60초
- **공격 사이클**: 2.32초
- **총 공격 횟수**: 25.86회
- **독립 DPS**: 37.93
- **비고**: 단일 몽타주 반복
---
## 🎯 신규 스토커 상세 분석: Cazimord
### 평타 DPS
- **BaseDamage**: 126.0
- **평타 DPS**: 69.57
- **콤보 시간**: 5.43초
- **평타 배율 합계**: 3.0
**평타 콤보 구성**:
1타: 1.67초, 배율 0.85
2타: 1.9초, 배율 1.05
3타: 1.87초, 배율 1.1
### 스킬 로테이션 DPS (30초)
- **로테이션 DPS**: 36.2
- **스킬 피해**: 327.6
- **평타 피해**: 758.31
- **평타 사용 시간**: 10.9초
**스킬 사용 내역**:
- 섬광: 2회, 총 피해 126.0
- 날개 베기: 2회, 총 피해 75.6
- 작열: 1회, 총 피해 126.0
### 버스트 DPS (10초)
- **버스트 DPS**: 23.14
- **궁극기 피해**: 100.8
- **스킬 피해**: 126.0
- **평타 피해**: 4.64
**스킬 사용 순서**:
5.5초: 마석 '칼날폭풍' (피해 100.8)
9.93초: 작열 (피해 126.0)
---
**작성자**: AI-assisted Analysis System
**생성 시각**: 2025-10-27 17:46:37

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개 항목이 검증을 통과했습니다.

View File

@ -0,0 +1,839 @@
{
"hilda": {
"scenario1": {
"dps": 78.83,
"combo_time": 4.57,
"total_multiplier": 3.0,
"base_damage": 120.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Hilda_B_Attack_W01_01",
"duration": 1.6,
"multiplier": 1.0
},
{
"index": 2,
"name": "AM_PC_Hilda_B_Attack_W01_02",
"duration": 1.6,
"multiplier": 1.05
},
{
"index": 3,
"name": "AM_PC_Hilda_B_Attack_W01_03",
"duration": 1.37,
"multiplier": 0.95
}
],
"notes": "",
"rank": 2
},
"scenario2": {
"dps": 67.3,
"duration": 30.0,
"base_damage": 120.0,
"skill_damage": 600.0,
"basic_damage": 1418.94,
"basic_attack_time": 18.0,
"skill_usage": {
"SK100202": {
"name": "반격",
"count": 3,
"damage": 288.0
},
"SK100201": {
"name": "칼날 격돌",
"count": 2,
"damage": 312.0
}
},
"notes": "",
"rank": 5
},
"scenario3": {
"dps": 46.13,
"duration": 10.0,
"base_damage": 120.0,
"ultimate_damage": 60.0,
"skill_damage": 252.0,
"basic_damage": 149.34,
"remaining_time": 1.89,
"skill_order": [
{
"time": 3.5,
"skill": "마석 ‘핏빛 달’",
"damage": 60.0,
"type": "ultimate"
},
{
"time": 5.3,
"skill": "칼날 격돌",
"damage": 156.0,
"type": "skill"
},
{
"time": 8.11,
"skill": "반격",
"damage": 96.0,
"type": "skill"
}
],
"has_ultimate": true,
"notes": "",
"rank": 7
}
},
"urud": {
"scenario1": {
"dps": 36.55,
"combo_time": 3.28,
"total_multiplier": 1.0,
"base_damage": 120.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Urud_Base_B_Attack_N",
"duration": 3.28,
"multiplier": 1.0
}
],
"notes": "",
"rank": 10
},
"scenario2": {
"dps": 74.26,
"duration": 30.0,
"base_damage": 120.0,
"skill_damage": 2184.0,
"basic_damage": 43.86,
"basic_attack_time": 1.2,
"skill_usage": {
"SK110101": {
"name": "화살 찌르기",
"count": 26,
"damage": 2184.0
}
},
"notes": "",
"rank": 2
},
"scenario3": {
"dps": 51.08,
"duration": 10.0,
"base_damage": 120.0,
"ultimate_damage": 120.0,
"skill_damage": 312.0,
"basic_damage": 78.79,
"remaining_time": 2.16,
"skill_order": [
{
"time": 3.5,
"skill": "마석 ‘폭쇄’",
"damage": 120.0,
"type": "ultimate"
},
{
"time": 5.12,
"skill": "독성 화살",
"damage": 120.0,
"type": "skill"
},
{
"time": 6.73,
"skill": "다발 화살",
"damage": 108.0,
"type": "skill"
},
{
"time": 7.84,
"skill": "화살 찌르기",
"damage": 84.0,
"type": "skill"
}
],
"has_ultimate": true,
"notes": "",
"rank": 5
}
},
"nave": {
"scenario1": {
"dps": 70.0,
"combo_time": 3.3,
"total_multiplier": 2.0,
"base_damage": 115.5,
"attacks": [
{
"index": 1,
"name": "AM_PC_Nave_B_Attack_W01_01",
"duration": 1.6,
"multiplier": 1.0
},
{
"index": 2,
"name": "AM_PC_Nave_B_Attack_W01_02",
"duration": 1.7,
"multiplier": 1.0
}
],
"notes": "",
"rank": 5
},
"scenario2": {
"dps": 50.27,
"duration": 30.0,
"base_damage": 115.5,
"skill_damage": 381.15,
"basic_damage": 1127.0,
"basic_attack_time": 16.1,
"skill_usage": {
"SK120201": {
"name": "마법 화살",
"count": 1,
"damage": 92.4
},
"SK120202": {
"name": "화염구",
"count": 1,
"damage": 231.0
},
"SK120206": {
"name": "노대바람",
"count": 1,
"damage": 57.75
}
},
"notes": "",
"rank": 7
},
"scenario3": {
"dps": 33.66,
"duration": 10.0,
"base_damage": 115.5,
"ultimate_damage": 115.5,
"skill_damage": 57.75,
"basic_damage": 163.33,
"remaining_time": 2.33,
"skill_order": [
{
"time": 6.33,
"skill": "마석 ‘해방’",
"damage": 115.5,
"type": "ultimate"
},
{
"time": 7.67,
"skill": "노대바람",
"damage": 57.75,
"type": "skill"
}
],
"has_ultimate": true,
"notes": "",
"rank": 9
}
},
"baran": {
"scenario1": {
"dps": 72.43,
"combo_time": 5.57,
"total_multiplier": 3.2,
"base_damage": 126.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Baran_B_Attack_W01_01",
"duration": 1.9,
"multiplier": 1.05
},
{
"index": 2,
"name": "AM_PC_Baran_B_Attack_W01_02",
"duration": 1.93,
"multiplier": 1.1
},
{
"index": 3,
"name": "AM_PC_Baran_B_Attack_W01_03",
"duration": 1.73,
"multiplier": 1.05
}
],
"notes": "",
"rank": 4
},
"scenario2": {
"dps": 71.31,
"duration": 30.0,
"base_damage": 126.0,
"skill_damage": 611.1,
"basic_damage": 1528.27,
"basic_attack_time": 21.1,
"skill_usage": {
"SK130206": {
"name": "깊게 찌르기",
"count": 2,
"damage": 277.2
},
"SK130203": {
"name": "후려치기",
"count": 2,
"damage": 302.4
},
"SK130204": {
"name": "갈고리 투척",
"count": 1,
"damage": 31.5
}
},
"notes": "",
"rank": 3
},
"scenario3": {
"dps": 65.93,
"duration": 10.0,
"base_damage": 126.0,
"ultimate_damage": 0,
"skill_damage": 321.3,
"basic_damage": 338.04,
"remaining_time": 4.67,
"skill_order": [
{
"time": 1.89,
"skill": "후려치기",
"damage": 151.2,
"type": "skill"
},
{
"time": 3.63,
"skill": "깊게 찌르기",
"damage": 138.6,
"type": "skill"
},
{
"time": 5.33,
"skill": "갈고리 투척",
"damage": 31.5,
"type": "skill"
}
],
"has_ultimate": true,
"notes": "",
"rank": 1
}
},
"rio": {
"scenario1": {
"dps": 76.58,
"combo_time": 3.87,
"total_multiplier": 2.35,
"base_damage": 126.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Rio_B_Attack_W01_01",
"duration": 1.17,
"multiplier": 0.7
},
{
"index": 2,
"name": "AM_PC_Rio_B_Attack_W01_02",
"duration": 1.33,
"multiplier": 0.8
},
{
"index": 3,
"name": "AM_PC_Rio_B_Attack_W01_03",
"duration": 1.37,
"multiplier": 0.85
}
],
"notes": "",
"rank": 3
},
"scenario2": {
"dps": 68.13,
"duration": 30.0,
"base_damage": 126.0,
"skill_damage": 2028.6,
"basic_damage": 15.32,
"basic_attack_time": 0.2,
"skill_usage": {
"SK140101": {
"name": "내려 찍기",
"count": 23,
"damage": 2028.6
}
},
"notes": "",
"rank": 4
},
"scenario3": {
"dps": 46.86,
"duration": 10.0,
"base_damage": 126.0,
"ultimate_damage": 37.8,
"skill_damage": 378.0,
"basic_damage": 52.82,
"remaining_time": 0.69,
"skill_order": [
{
"time": 3.5,
"skill": "마석 ‘민감’",
"damage": 37.8,
"type": "ultimate"
},
{
"time": 4.91,
"skill": "연속 찌르기",
"damage": 126.0,
"type": "skill"
},
{
"time": 5.48,
"skill": "접근",
"damage": 126.0,
"type": "skill"
},
{
"time": 9.31,
"skill": "단검 투척",
"damage": 126.0,
"type": "skill"
}
],
"has_ultimate": true,
"notes": "",
"rank": 6
}
},
"clad": {
"scenario1": {
"dps": 47.88,
"combo_time": 4.17,
"total_multiplier": 2.1,
"base_damage": 95.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Clad_Base_Attack_Mace1",
"duration": 1.9,
"multiplier": 1.05
},
{
"index": 2,
"name": "AM_PC_Clad_Base_Attack_Mace2",
"duration": 2.27,
"multiplier": 1.05
}
],
"notes": "",
"rank": 8
},
"scenario2": {
"dps": 60.1,
"duration": 30.0,
"base_damage": 95.0,
"skill_damage": 855.0,
"basic_damage": 948.02,
"basic_attack_time": 19.8,
"skill_usage": {
"SK150201": {
"name": "다시 흙으로",
"count": 6,
"damage": 855.0
}
},
"notes": "",
"rank": 6
},
"scenario3": {
"dps": 53.99,
"duration": 10.0,
"base_damage": 95.0,
"ultimate_damage": 0,
"skill_damage": 142.5,
"basic_damage": 397.4,
"remaining_time": 8.3,
"skill_order": [
{
"time": 1.7,
"skill": "다시 흙으로",
"damage": 142.5,
"type": "skill"
}
],
"has_ultimate": false,
"notes": "",
"rank": 3
}
},
"rene": {
"scenario1": {
"dps": 55.93,
"combo_time": 5.9,
"total_multiplier": 3.0,
"base_damage": 110.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Rene_B_Attack_W01_01",
"duration": 1.9,
"multiplier": 1.0
},
{
"index": 2,
"name": "AM_PC_Rene_B_Attack_W01_02",
"duration": 1.8,
"multiplier": 1.0
},
{
"index": 3,
"name": "AM_PC_Rene_B_Attack_W01_03",
"duration": 2.2,
"multiplier": 1.0
}
],
"notes": "",
"rank": 7
},
"scenario2": {
"dps": 30.44,
"duration": 30.0,
"base_damage": 110.0,
"skill_damage": 907.5,
"basic_damage": 5.59,
"basic_attack_time": 0.1,
"skill_usage": {
"SK160101": {
"name": "할퀴기",
"count": 11,
"damage": 907.5
}
},
"notes": "",
"rank": 9
},
"scenario3": {
"dps": 34.69,
"duration": 10.0,
"base_damage": 110.0,
"ultimate_damage": 0,
"skill_damage": 242.0,
"basic_damage": 104.87,
"remaining_time": 1.88,
"skill_order": [
{
"time": 1.46,
"skill": "정령 소환 : 화염",
"damage": 132.0,
"type": "skill"
},
{
"time": 8.12,
"skill": "독기 화살",
"damage": 110.0,
"type": "skill"
}
],
"has_ultimate": false,
"notes": "",
"rank": 8
},
"summon_analysis": {
"SK160202": {
"name": "정령 소환: 화염",
"summon": "Ifrit",
"active_duration": 20,
"cycle_time": 1.46,
"attack_count": 13.71,
"dps": 90.51,
"notes": "1개 몽타주 순차 루프"
},
"SK160206": {
"name": "정령 소환: 냉기",
"summon": "Shiva",
"active_duration": 60,
"cycle_time": 2.32,
"attack_count": 25.86,
"dps": 37.93,
"notes": "단일 몽타주 반복"
}
}
},
"sinobu": {
"scenario1": {
"dps": 88.94,
"combo_time": 2.27,
"total_multiplier": 1.6,
"base_damage": 126.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Sinobu_B_Attack_W01_03",
"duration": 1.07,
"multiplier": 0.8
},
{
"index": 2,
"name": "AM_PC_Sinobu_B_Attack_W01_01",
"duration": 1.2,
"multiplier": 0.8
}
],
"notes": "",
"rank": 1
},
"scenario2": {
"dps": 80.94,
"duration": 30.0,
"base_damage": 126.0,
"skill_damage": 2268.0,
"basic_damage": 160.09,
"basic_attack_time": 1.8,
"skill_usage": {
"SK180101": {
"name": "표창",
"count": 15,
"damage": 2268.0
}
},
"notes": "",
"rank": 1
},
"scenario3": {
"dps": 55.82,
"duration": 10.0,
"base_damage": 126.0,
"ultimate_damage": 0.0,
"skill_damage": 453.6,
"basic_damage": 104.61,
"remaining_time": 1.18,
"skill_order": [
{
"time": 2.33,
"skill": "마석 '반환'",
"damage": 0.0,
"type": "ultimate"
},
{
"time": 5.48,
"skill": "기폭찰",
"damage": 163.8,
"type": "skill"
},
{
"time": 7.36,
"skill": "표창",
"damage": 151.2,
"type": "skill"
},
{
"time": 8.82,
"skill": "비뢰각",
"damage": 138.6,
"type": "skill"
}
],
"has_ultimate": true,
"notes": "",
"rank": 2
}
},
"lian": {
"scenario1": {
"dps": 36.73,
"combo_time": 3.27,
"total_multiplier": 1.0,
"base_damage": 120.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Lian_Base_000_Attack_Bow",
"duration": 3.27,
"multiplier": 1.0
}
],
"notes": "",
"rank": 9
},
"scenario2": {
"dps": 16.46,
"duration": 30.0,
"base_damage": 120.0,
"skill_damage": 336.0,
"basic_damage": 157.94,
"basic_attack_time": 4.3,
"skill_usage": {
"SK190101": {
"name": "정조준",
"count": 4,
"damage": 336.0
}
},
"notes": "",
"rank": 10
},
"scenario3": {
"dps": 51.65,
"duration": 10.0,
"base_damage": 120.0,
"ultimate_damage": 0,
"skill_damage": 426.0,
"basic_damage": 90.49,
"remaining_time": 2.46,
"skill_order": [
{
"time": 2.67,
"skill": "비연사",
"damage": 180.0,
"type": "skill"
},
{
"time": 4.87,
"skill": "연화",
"damage": 144.0,
"type": "skill"
},
{
"time": 7.54,
"skill": "속사",
"damage": 102.0,
"type": "skill"
}
],
"has_ultimate": false,
"notes": "",
"rank": 4
}
},
"cazimord": {
"scenario1": {
"dps": 69.57,
"combo_time": 5.43,
"total_multiplier": 3.0,
"base_damage": 126.0,
"attacks": [
{
"index": 1,
"name": "AM_PC_Cazimord_B_Attack_W01_01",
"duration": 1.67,
"multiplier": 0.85
},
{
"index": 2,
"name": "AM_PC_Cazimord_B_Attack_W01_02",
"duration": 1.9,
"multiplier": 1.05
},
{
"index": 3,
"name": "AM_PC_Cazimord_B_Attack_W01_03",
"duration": 1.87,
"multiplier": 1.1
}
],
"notes": "",
"rank": 6
},
"scenario2": {
"dps": 36.2,
"duration": 30.0,
"base_damage": 126.0,
"skill_damage": 327.6,
"basic_damage": 758.31,
"basic_attack_time": 10.9,
"skill_usage": {
"SK170201": {
"name": "섬광",
"count": 2,
"damage": 126.0
},
"SK170202": {
"name": "날개 베기",
"count": 2,
"damage": 75.6
},
"SK170203": {
"name": "작열",
"count": 1,
"damage": 126.0
}
},
"notes": "",
"rank": 8
},
"scenario3": {
"dps": 23.14,
"duration": 10.0,
"base_damage": 126.0,
"ultimate_damage": 100.8,
"skill_damage": 126.0,
"basic_damage": 4.64,
"remaining_time": 0.07,
"skill_order": [
{
"time": 5.5,
"skill": "마석 '칼날폭풍'",
"damage": 100.8,
"type": "ultimate"
},
{
"time": 9.93,
"skill": "작열",
"damage": 126.0,
"type": "skill"
}
],
"has_ultimate": true,
"notes": "",
"rank": 10
}
},
"dot_analysis": {
"SK110204": {
"stalker": "urud",
"name": "독성 화살",
"dot_type": "Poison",
"description": "대상 MaxHP의 20% (5초간)",
"dps_by_hp": {
"100": 4.0,
"500": 20.0,
"1000": 40.0
}
},
"SK160203": {
"stalker": "rene",
"name": "독기 화살",
"dot_type": "Bleed",
"description": "고정 20 피해 (5초간)",
"dps_by_hp": {
"100": 4.0,
"500": 4.0,
"1000": 4.0
}
},
"SK170201": {
"stalker": "cazimord",
"name": "작열",
"dot_type": "Burn",
"description": "대상 MaxHP의 10% (3초간)",
"dps_by_hp": {
"100": 3.33,
"500": 16.67,
"1000": 33.33
}
},
"SK160202": {
"stalker": "rene",
"name": "정령 소환: 화염",
"dot_type": "Burn",
"description": "대상 MaxHP의 10% (3초간)",
"dps_by_hp": {
"100": 3.33,
"500": 16.67,
"1000": 33.33
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
캐릭터 스탯 분석 스크립트
DT_CharacterStat 테이블에서 스토커들의 기본 스탯을 추출하고 비교 분석합니다.
사용법:
python analyze_character_stats.py <DataTable.json 경로>
"""
import json
import sys
from pathlib import Path
def find_character_stat_table(datatables):
"""DT_CharacterStat 테이블 찾기"""
for dt in datatables:
if dt.get('AssetName') == 'DT_CharacterStat':
return dt
return None
def analyze_stats(json_path):
"""캐릭터 스탯 분석"""
with open(json_path, 'r', encoding='utf-8') as f:
datatables = json.load(f)
char_stat_table = find_character_stat_table(datatables)
if not char_stat_table:
print("오류: DT_CharacterStat 테이블을 찾을 수 없습니다.")
return
stalkers = []
for row in char_stat_table.get('Rows', []):
data = row['Data']
stalker_info = {
'id': row['RowName'],
'name': data.get('name', ''),
'job': data.get('jobName', ''),
'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)
}
stalkers.append(stalker_info)
return stalkers
def print_stat_table(stalkers):
"""스탯 테이블 출력"""
print("\n스토커별 기본 스탯")
print("=" * 100)
print(f"{'이름':<10} {'직업':<10} {'STR':>5} {'DEX':>5} {'INT':>5} {'CON':>5} {'WIS':>5} {'HP':>5} {'MP':>5}")
print("-" * 100)
for s in stalkers:
print(f"{s['name']:<10} {s['job']:<10} {s['str']:>5} {s['dex']:>5} {s['int']:>5} {s['con']:>5} {s['wis']:>5} {s['hp']:>5} {s['mp']:>5}")
def print_stat_rankings(stalkers):
"""스탯별 랭킹 출력"""
print("\n\n스탯별 랭킹")
print("=" * 100)
stats = ['str', 'dex', 'int', 'con', 'wis']
stat_names = {'str': 'STR', 'dex': 'DEX', 'int': 'INT', 'con': 'CON', 'wis': 'WIS'}
for stat in stats:
sorted_stalkers = sorted(stalkers, key=lambda x: x[stat], reverse=True)
top3 = sorted_stalkers[:3]
print(f"\n{stat_names[stat]} 순위:")
for i, s in enumerate(top3, 1):
print(f" {i}위: {s['name']} ({s[stat]})")
def main():
if len(sys.argv) < 2:
print("사용법: python analyze_character_stats.py <DataTable.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
if not json_path.exists():
print(f"오류: 파일을 찾을 수 없습니다: {json_path}")
sys.exit(1)
print(f"분석 중: {json_path}")
stalkers = analyze_stats(json_path)
if stalkers:
print(f"\n{len(stalkers)}명의 스토커 발견")
print_stat_table(stalkers)
print_stat_rankings(stalkers)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
GameplayEffect Blueprint 분석
"""
import json
import sys
from pathlib import Path
def extract_ge_name(ge_path):
"""GE 경로에서 이름 추출"""
return ge_path.split('/')[-1].split('.')[0]
def analyze_ge_blueprint(ge_asset):
"""GE Blueprint에서 중요 정보 추출"""
asset_name = ge_asset.get('AssetName', '')
# Variables 섹션 찾기
variables = []
if 'Variables' in ge_asset:
for var in ge_asset['Variables']:
var_name = var.get('Name', '')
var_value = var.get('Value', '')
var_type = var.get('Type', '')
variables.append({
'name': var_name,
'value': var_value,
'type': var_type
})
# EventGraph 섹션에서 로직 확인
event_graphs = []
if 'EventGraphs' in ge_asset:
for graph in ge_asset['EventGraphs']:
graph_name = graph.get('Name', '')
event_graphs.append(graph_name)
return {
'name': asset_name,
'variables': variables,
'event_graphs': event_graphs
}
def main():
if len(sys.argv) < 3:
print("사용법: python analyze_ge_blueprints.py <Blueprint.json> <ultimate_ge_list.json>")
sys.exit(1)
bp_path = Path(sys.argv[1])
ge_list_path = Path(sys.argv[2])
# GE 목록 로드
with open(ge_list_path, 'r', encoding='utf-8') as f:
ge_data = json.load(f)
target_ge_classes = ge_data['all_ge_classes']
target_ge_names = [extract_ge_name(ge) for ge in target_ge_classes]
print(f"Blueprint 로딩 중: {bp_path} (24MB, 시간 소요)")
with open(bp_path, 'r', encoding='utf-8') as f:
bp_data = json.load(f)
assets = bp_data.get('Assets', [])
print(f"{len(assets)}개 Blueprint Assets 로드 완료")
print("\n" + "=" * 100)
print("GameplayEffect Blueprint 상세 분석")
print("=" * 100)
results = {}
for target_name in target_ge_names:
# Blueprint에서 GE 찾기
ge_asset = next((a for a in assets if a.get('AssetName', '') == target_name), None)
if not ge_asset:
print(f"\n{target_name}")
print(f" → Blueprint에서 찾을 수 없음")
continue
analysis = analyze_ge_blueprint(ge_asset)
results[target_name] = analysis
print(f"\n{target_name}")
if analysis['variables']:
print(f" Variables: {len(analysis['variables'])}")
for var in analysis['variables']:
print(f" - {var['name']} ({var['type']}): {var['value']}")
else:
print(f" Variables: 없음")
if analysis['event_graphs']:
print(f" EventGraphs: {', '.join(analysis['event_graphs'])}")
# 결과 저장
output_file = ge_list_path.parent / "ge_blueprint_analysis.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\n\n분석 결과 저장: {output_file}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
궁극기 GameplayEffectSet 확인
"""
import json
import sys
from pathlib import Path
STALKERS = ['hilda', 'urud', 'nave', 'baran', 'rio', 'clad', 'rene', 'sinobu', 'lian', 'cazimord']
def main():
if len(sys.argv) < 2:
print("사용법: python check_ultimate_effects.py <DataTable.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
# DT_CharacterStat에서 궁극기 ID 추출
dt_char_stat = next((dt for dt in assets if dt.get('AssetName') == 'DT_CharacterStat'), None)
stalker_ultimates = {}
for row in dt_char_stat.get('Rows', []):
row_name = row['RowName']
if row_name in STALKERS:
stalker_ultimates[row_name] = row['Data'].get('ultimateSkill', '')
# DT_Skill에서 궁극기 정보 확인
dt_skill = next((dt for dt in assets if dt.get('AssetName') == 'DT_Skill'), None)
print("=" * 100)
print("궁극기 GameplayEffectSet 확인")
print("=" * 100)
for stalker in STALKERS:
ult_id = stalker_ultimates.get(stalker, '')
if not ult_id:
continue
skill_row = next((row for row in dt_skill['Rows'] if row['RowName'] == ult_id), None)
if not skill_row:
continue
data_field = skill_row['Data']
print(f"\n{stalker.upper()}{ult_id}")
print(f" 이름: {data_field.get('name', 'N/A')}")
print(f" 간단 설명: {data_field.get('simpleDesc', 'N/A')}")
print(f" skillDamageRate: {data_field.get('skillDamageRate', 0)}")
print(f" skillAttackType: {data_field.get('skillAttackType', 'N/A')}")
effect_set = data_field.get('gameplayEffectSet', [])
if effect_set:
print(f" gameplayEffectSet: {len(effect_set)}개 효과")
for i, effect in enumerate(effect_set, 1):
print(f" [{i}] {effect}")
else:
print(f" gameplayEffectSet: 비어있음 → Blueprint 확인 필요")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Activation Order Group 추출 스크립트
Blueprint.json에서 스토커별 스킬의 ActivationOrderGroup 값을 추출합니다.
사용법:
python extract_activation_order_groups.py <Blueprint.json 경로>
"""
import json
import sys
from pathlib import Path
from collections import defaultdict
def extract_activation_order_groups(json_path):
"""Blueprint.json에서 ActivationOrderGroup 추출"""
with open(json_path, 'r', encoding='utf-8') as f:
blueprints = json.load(f)
# 스토커별 스킬 그룹화
stalker_skills = defaultdict(list)
stalkers = ['Hilda', 'Urud', 'Nave', 'Baran', 'Rio', 'Clad', 'Rene', 'Sinobu', 'Lian', 'Cazimord']
for bp in blueprints:
asset_name = bp.get('AssetName', '')
# GA_Skill_{Stalker}_ 패턴 찾기
if asset_name.startswith('GA_Skill_'):
for stalker in stalkers:
if f'_{stalker}_' in asset_name:
# ActivationOrderGroup 찾기
activation_order = None
for var in bp.get('Variables', []):
if var.get('Name') == 'ActivationOrderGroup':
activation_order = var.get('DefaultValue', '0')
break
skill_name = asset_name.replace(f'GA_Skill_{stalker}_', '')
stalker_skills[stalker].append({
'skill': skill_name,
'order_group': int(activation_order) if activation_order else 0,
'full_name': asset_name
})
return stalker_skills
def print_stalker_skills(stalker_skills):
"""스토커별 스킬과 ActivationOrderGroup 출력"""
print("\n스토커별 Activation Order Group")
print("=" * 100)
for stalker, skills in sorted(stalker_skills.items()):
print(f"\n{stalker}:")
# Order Group별로 정렬
skills_by_group = defaultdict(list)
for skill in skills:
skills_by_group[skill['order_group']].append(skill['skill'])
for group in sorted(skills_by_group.keys(), reverse=True):
print(f" Group {group}: {', '.join(sorted(skills_by_group[group]))}")
def print_statistics(stalker_skills):
"""통계 정보 출력"""
print("\n\n통계")
print("=" * 100)
# 각 Group별 사용 빈도
group_count = defaultdict(int)
for stalker, skills in stalker_skills.items():
for skill in skills:
group_count[skill['order_group']] += 1
print("\nGroup별 스킬 수:")
for group in sorted(group_count.keys(), reverse=True):
print(f" Group {group}: {group_count[group]}")
# 스토커별 스킬 수
print("\n스토커별 스킬 수:")
for stalker, skills in sorted(stalker_skills.items(), key=lambda x: len(x[1]), reverse=True):
print(f" {stalker}: {len(skills)}")
def main():
if len(sys.argv) < 2:
print("사용법: python extract_activation_order_groups.py <Blueprint.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
if not json_path.exists():
print(f"오류: 파일을 찾을 수 없습니다: {json_path}")
sys.exit(1)
print(f"분석 중: {json_path}")
stalker_skills = extract_activation_order_groups(json_path)
print(f"\n{sum(len(skills) for skills in stalker_skills.values())}개의 스킬 발견")
print_stalker_skills(stalker_skills)
print_statistics(stalker_skills)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""
모든 궁극기 상세 정보 추출 (desc 포함)
"""
import json
import sys
from pathlib import Path
STALKERS = ['hilda', 'urud', 'nave', 'baran', 'rio', 'clad', 'rene', 'sinobu', 'lian', 'cazimord']
def main():
if len(sys.argv) < 2:
print("사용법: python extract_all_ultimates_detailed.py <DataTable.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
# DT_CharacterStat에서 궁극기 ID 추출
dt_char_stat = next((dt for dt in assets if dt.get('AssetName') == 'DT_CharacterStat'), None)
stalker_ultimates = {}
for row in dt_char_stat.get('Rows', []):
row_name = row['RowName']
if row_name in STALKERS:
stalker_ultimates[row_name] = row['Data'].get('ultimateSkill', '')
# DT_Skill에서 궁극기 정보 확인
dt_skill = next((dt for dt in assets if dt.get('AssetName') == 'DT_Skill'), None)
print("=" * 120)
print("모든 궁극기 상세 정보")
print("=" * 120)
for stalker in STALKERS:
ult_id = stalker_ultimates.get(stalker, '')
if not ult_id:
continue
skill_row = next((row for row in dt_skill['Rows'] if row['RowName'] == ult_id), None)
if not skill_row:
continue
d = skill_row['Data']
print(f"\n{'='*120}")
print(f"{stalker.upper()}{ult_id}")
print(f"{'='*120}")
print(f"이름: {d.get('name', 'N/A')}")
print(f"설명: {d.get('desc', 'N/A')}")
print(f"간단 설명: {d.get('simpleDesc', 'N/A')}")
print(f"\n기본 정보:")
print(f" - bIsUltimate: {d.get('bIsUltimate', False)}")
print(f" - skillDamageRate: {d.get('skillDamageRate', 0)}")
print(f" - skillAttackType: {d.get('skillAttackType', 'N/A')}")
print(f" - skillElementType: {d.get('skillElementType', 'N/A')}")
print(f" - castingTime: {d.get('castingTime', 0)}")
print(f" - activeDuration: {d.get('activeDuration', 0)}")
print(f" - manaCost: {d.get('manaCost', 0)}")
print(f" - coolTime: {d.get('coolTime', 0)}")
effect_set = d.get('gameplayEffectSet', [])
if effect_set:
print(f"\ngameplayEffectSet: {len(effect_set)}개 효과")
for i, effect in enumerate(effect_set, 1):
ge_class = effect.get('gEClass', '')
trigger = effect.get('trigger', '')
tag_values = effect.get('gETagValues', [])
# Ignore 효과는 간단히 표시
if 'Ignore' in ge_class:
print(f" [{i}] {trigger}: {ge_class.split('/')[-1].split('.')[0]} (면역 효과)")
else:
ge_name = ge_class.split('/')[-1].split('.')[0]
print(f" [{i}] {trigger}: {ge_name}")
if tag_values:
for tv in tag_values:
tag_name = tv.get('tag', {}).get('tagName', 'Unknown')
value = tv.get('value', 0)
print(f"{tag_name}: {value}")
else:
print(f"\ngameplayEffectSet: 비어있음")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
궁극기에서 사용하는 GameplayEffect 추출
"""
import json
import sys
from pathlib import Path
STALKERS = ['hilda', 'urud', 'nave', 'baran', 'rio', 'clad', 'rene', 'sinobu', 'lian', 'cazimord']
def main():
if len(sys.argv) < 2:
print("사용법: python extract_ge_from_ultimates.py <DataTable.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
# DT_CharacterStat에서 궁극기 ID 추출
dt_char_stat = next((dt for dt in assets if dt.get('AssetName') == 'DT_CharacterStat'), None)
stalker_ultimates = {}
for row in dt_char_stat.get('Rows', []):
row_name = row['RowName']
if row_name in STALKERS:
stalker_ultimates[row_name] = row['Data'].get('ultimateSkill', '')
# DT_Skill에서 궁극기 gameplayEffectSet 추출
dt_skill = next((dt for dt in assets if dt.get('AssetName') == 'DT_Skill'), None)
all_ge_classes = set()
stalker_ges = {}
print("=" * 100)
print("궁극기에서 사용하는 GameplayEffect 목록")
print("=" * 100)
for stalker in STALKERS:
ult_id = stalker_ultimates.get(stalker, '')
if not ult_id:
continue
skill_row = next((row for row in dt_skill['Rows'] if row['RowName'] == ult_id), None)
if not skill_row:
continue
data_field = skill_row['Data']
effect_set = data_field.get('gameplayEffectSet', [])
stalker_ges[stalker] = []
print(f"\n{stalker.upper()}{ult_id} - {data_field.get('name', 'N/A')}")
if not effect_set:
print(f" → gameplayEffectSet 비어있음")
continue
for effect in effect_set:
ge_class = effect.get('gEClass', '')
trigger = effect.get('trigger', '')
tag_values = effect.get('gETagValues', [])
# Ignore 효과는 스킵
if 'Ignore' in ge_class:
continue
# GE 클래스 이름 추출
if ge_class:
ge_name = ge_class.split('/')[-1].replace('.', '_').replace('_C', '')
all_ge_classes.add(ge_class)
stalker_ges[stalker].append({
'name': ge_name,
'class': ge_class,
'trigger': trigger,
'tagValues': tag_values
})
print(f" [{trigger}] {ge_name}")
if tag_values:
for tv in tag_values:
tag_name = tv.get('tag', {}).get('tagName', 'Unknown')
value = tv.get('value', 0)
print(f" - {tag_name}: {value}")
print(f"\n\n{len(all_ge_classes)}개의 고유한 GE 클래스 발견")
print("\nGE 클래스 목록:")
for ge in sorted(all_ge_classes):
ge_name = ge.split('/')[-1].replace('.', '_').replace('_C', '')
print(f" - {ge_name}")
# JSON 파일로 저장
output_file = Path(sys.argv[1]).parent / "ultimate_ge_list.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump({
'stalker_ges': stalker_ges,
'all_ge_classes': list(all_ge_classes)
}, f, ensure_ascii=False, indent=2)
print(f"\n\n결과 저장: {output_file}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
스킬 캔슬 윈도우 추출 스크립트
AnimMontage.json에서 ANS_SkillCancel_C 노티파이를 가진 몽타주를 찾아
캔슬 가능 시간 구간을 추출합니다.
사용법:
python extract_skill_cancel_windows.py <AnimMontage.json 경로>
"""
import json
import sys
from pathlib import Path
def extract_cancel_windows(json_path):
"""AnimMontage.json에서 스킬 캔슬 윈도우 추출"""
with open(json_path, 'r', encoding='utf-8') as f:
montages = json.load(f)
cancel_montages = []
for montage in montages:
asset_name = montage.get('AssetName', '')
# ANS_SkillCancel_C 노티파이 찾기
for notify in montage.get('AnimNotifies', []):
if notify.get('NotifyStateClass') == 'ANS_SkillCancel_C':
trigger_time = notify['TriggerTime']
duration = notify['Duration']
cancel_montages.append({
'montage': asset_name,
'start': trigger_time,
'end': trigger_time + duration,
'duration': duration
})
return cancel_montages
def main():
if len(sys.argv) < 2:
print("사용법: python extract_skill_cancel_windows.py <AnimMontage.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
if not json_path.exists():
print(f"오류: 파일을 찾을 수 없습니다: {json_path}")
sys.exit(1)
print(f"분석 중: {json_path}")
print("-" * 80)
cancel_windows = extract_cancel_windows(json_path)
print(f"\n{len(cancel_windows)}개의 스킬 캔슬 윈도우 발견\n")
for item in cancel_windows:
print(f"{item['montage']}")
print(f" 캔슬 구간: {item['start']:.3f}s ~ {item['end']:.3f}s (지속: {item['duration']:.3f}s)")
print()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""
스토커 기본 데이터 추출 스크립트
DT_CharacterStat, DT_CharacterAbility, DT_Skill에서
10명 스토커의 모든 정보를 추출합니다.
"""
import json
import sys
from pathlib import Path
STALKERS = ['hilda', 'urud', 'nave', 'baran', 'rio', 'clad', 'rene', 'sinobu', 'lian', 'cazimord']
def load_json(file_path):
"""JSON 파일 로드"""
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# Assets 배열 반환
return data.get('Assets', [])
def find_table(datatables, table_name):
"""특정 테이블 찾기"""
for dt in datatables:
if dt.get('AssetName') == table_name:
return dt
return None
def extract_character_stats(datatables):
"""DT_CharacterStat에서 스토커 기본 정보 추출"""
char_stat_table = find_table(datatables, 'DT_CharacterStat')
if not char_stat_table:
return {}
stalker_data = {}
for row in char_stat_table.get('Rows', []):
row_name = row['RowName']
if row_name in STALKERS:
data = row['Data']
stalker_data[row_name] = {
'name': data.get('name', ''),
'jobName': data.get('jobName', ''),
'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': data.get('manaRegen', 0),
'physicalDamage': data.get('physicalDamage', 0),
'magicalDamage': data.get('magicalDamage', 0),
'defaultSkills': data.get('defaultSkills', []),
'subSkill': data.get('subSkill', ''),
'ultimateSkill': data.get('ultimateSkill', ''),
'equipableTypes': data.get('equipableTypes', []),
'ultimatePoint': data.get('ultimatePoint', 0)
}
return stalker_data
def extract_character_abilities(datatables):
"""DT_CharacterAbility에서 평타 몽타주 추출"""
char_ability_table = find_table(datatables, 'DT_CharacterAbility')
if not char_ability_table:
return {}
stalker_abilities = {}
for row in char_ability_table.get('Rows', []):
row_name = row['RowName']
if row_name in STALKERS:
data = row['Data']
attack_montage_map = data.get('attackMontageMap', {})
stalker_abilities[row_name] = {
'attackMontageMap': attack_montage_map,
'abilities': data.get('abilities', [])
}
return stalker_abilities
def extract_skills(datatables, stalker_stats):
"""DT_Skill에서 스토커별 스킬 정보 추출"""
skill_table = find_table(datatables, 'DT_Skill')
if not skill_table:
return {}
# 모든 스킬을 ID로 매핑
all_skills = {}
for row in skill_table.get('Rows', []):
skill_id = row['RowName']
data = row['Data']
all_skills[skill_id] = {
'skillId': skill_id,
'stalkerName': data.get('stalkerName', ''),
'name': data.get('name', ''),
'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),
'useMontages': data.get('useMontages', []),
'abilityClass': data.get('abilityClass', '')
}
# 스토커별로 스킬 그룹화
stalker_skills = {}
for stalker_id, stats in stalker_stats.items():
skills = {
'defaultSkills': [],
'subSkill': None,
'ultimateSkill': None
}
# 기본 스킬
for skill_id in stats['defaultSkills']:
if skill_id in all_skills:
skills['defaultSkills'].append(all_skills[skill_id])
# 서브 스킬
if stats['subSkill'] in all_skills:
skills['subSkill'] = all_skills[stats['subSkill']]
# 궁극기
if stats['ultimateSkill'] in all_skills:
skills['ultimateSkill'] = all_skills[stats['ultimateSkill']]
stalker_skills[stalker_id] = skills
return stalker_skills
def main():
if len(sys.argv) < 2:
print("사용법: python extract_stalker_data.py <DataTable.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
if not json_path.exists():
print(f"오류: 파일을 찾을 수 없습니다: {json_path}")
sys.exit(1)
print(f"분석 중: {json_path}")
datatables = load_json(json_path)
print("\n=== 스토커 기본 스탯 추출 ===")
stalker_stats = extract_character_stats(datatables)
print(f"추출 완료: {len(stalker_stats)}")
print("\n=== 스토커 평타 몽타주 추출 ===")
stalker_abilities = extract_character_abilities(datatables)
print(f"추출 완료: {len(stalker_abilities)}")
print("\n=== 스토커 스킬 정보 추출 ===")
stalker_skills = extract_skills(datatables, stalker_stats)
print(f"추출 완료: {len(stalker_skills)}")
# 결과 출력
for stalker_id in STALKERS:
if stalker_id not in stalker_stats:
continue
stats = stalker_stats[stalker_id]
print(f"\n{'='*80}")
print(f"{stats['name']}】 ({stats['jobName']})")
print(f"{'='*80}")
print(f"STR: {stats['str']:2d} | DEX: {stats['dex']:2d} | INT: {stats['int']:2d} | CON: {stats['con']:2d} | WIS: {stats['wis']:2d}")
print(f"HP: {stats['hP']} | MP: {stats['mP']} | Mana Regen: {stats['manaRegen']}")
print(f"장착 가능: {', '.join(stats['equipableTypes'])}")
print(f"궁극기 포인트: {stats['ultimatePoint']}")
# 스킬 정보
if stalker_id in stalker_skills:
skills = stalker_skills[stalker_id]
print(f"\n[기본 스킬]")
for skill in skills['defaultSkills']:
print(f" - {skill['name']} ({skill['skillId']}): {skill['skillAttackType']} | 쿨타임: {skill['coolTime']}초 | 마나: {skill['manaCost']}")
if skills['subSkill']:
skill = skills['subSkill']
print(f"\n[서브 스킬]")
print(f" - {skill['name']} ({skill['skillId']}): {skill['skillAttackType']} | 쿨타임: {skill['coolTime']}")
if skills['ultimateSkill']:
skill = skills['ultimateSkill']
print(f"\n[궁극기]")
print(f" - {skill['name']} ({skill['skillId']}): {skill['skillAttackType']}")
# 평타 몽타주
if stalker_id in stalker_abilities:
abilities = stalker_abilities[stalker_id]
attack_map = abilities.get('attackMontageMap', {})
if attack_map:
print(f"\n[평타 몽타주]")
for weapon_type, montage_data in attack_map.items():
montage_array = montage_data.get('montageArray', [])
print(f" - {weapon_type}: {len(montage_array)}타 연속")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
궁극기 정보 추출 스크립트
모든 스토커의 궁극기 정보를 DT_Skill에서 추출합니다.
"""
import json
import sys
from pathlib import Path
STALKERS = ['hilda', 'urud', 'nave', 'baran', 'rio', 'clad', 'rene', 'sinobu', 'lian', 'cazimord']
def load_json(file_path):
"""JSON 파일 로드"""
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data.get('Assets', [])
def find_table(datatables, table_name):
"""특정 테이블 찾기"""
for dt in datatables:
if dt.get('AssetName') == table_name:
return dt
return None
def main():
if len(sys.argv) < 2:
print("사용법: python extract_ultimate_skills.py <DataTable.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
datatables = load_json(json_path)
# DT_CharacterStat에서 궁극기 ID 추출
char_stat_table = find_table(datatables, 'DT_CharacterStat')
stalker_ultimates = {}
for row in char_stat_table.get('Rows', []):
row_name = row['RowName']
if row_name in STALKERS:
stalker_ultimates[row_name] = row['Data'].get('ultimateSkill', '')
# DT_Skill에서 궁극기 상세 정보 추출
skill_table = find_table(datatables, 'DT_Skill')
print("=" * 100)
print("스토커별 궁극기 정보")
print("=" * 100)
for stalker in STALKERS:
ult_id = stalker_ultimates.get(stalker, '')
if not ult_id:
continue
# 스킬 정보 찾기
skill_row = next((row for row in skill_table['Rows'] if row['RowName'] == ult_id), None)
if not skill_row:
print(f"\n{stalker}: 궁극기 {ult_id} 정보를 찾을 수 없습니다")
continue
data = skill_row['Data']
print(f"\n{stalker.upper()}")
print(f" ID: {ult_id}")
print(f" 이름: {data.get('name', 'N/A')}")
print(f" 간단 설명: {data.get('simpleDesc', 'N/A')}")
print(f" bIsUltimate: {data.get('bIsUltimate', False)}")
print(f" skillDamageRate: {data.get('skillDamageRate', 0)}")
print(f" skillAttackType: {data.get('skillAttackType', 'N/A')}")
print(f" skillElementType: {data.get('skillElementType', 'N/A')}")
print(f" castingTime: {data.get('castingTime', 0)}")
print(f" activeDuration: {data.get('activeDuration', 0)}")
print(f" manaCost: {data.get('manaCost', 0)}")
print(f" coolTime: {data.get('coolTime', 0)}")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""
GA_Skill Blueprint 찾기
"""
import json
import sys
from pathlib import Path
def main():
if len(sys.argv) < 2:
print("사용법: python find_ga_skills.py <Blueprint.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
print(f"로딩 중: {json_path}")
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
print(f"총 Blueprint Assets: {len(assets)}")
# GA_Skill 찾기 (대소문자 무시)
ga_skills = [a for a in assets if 'ga_skill' in a.get('AssetName', '').lower()]
print(f"GA_Skill Assets: {len(ga_skills)}")
# 스토커별 분류
stalkers = ['Hilda', 'Urud', 'Nave', 'Baran', 'Rio', 'Clad', 'Rene', 'Sinobu', 'Lian', 'Cazimord']
for stalker in stalkers:
stalker_gas = [a for a in ga_skills if stalker.lower() in a.get('AssetName', '').lower()]
if stalker_gas:
print(f"\n{stalker} GA_Skills: {len(stalker_gas)}")
for ga in stalker_gas[:10]: # 최대 10개만 표시
asset_name = ga.get('AssetName', 'UNKNOWN')
print(f" - {asset_name}")
if len(stalker_gas) > 10:
print(f" ... 외 {len(stalker_gas) - 10}개 더")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""
특정 스토커들의 스킬 정보를 DT_Skill에서 정확히 추출
"""
import json
import sys
from pathlib import Path
# 문제가 있는 스토커들
STALKERS = ['nave', 'baran', 'rio', 'clad', 'rene', 'sinobu', 'lian']
def main():
if len(sys.argv) < 2:
print("사용법: python verify_skills_detailed.py <DataTable.json 경로>")
sys.exit(1)
json_path = Path(sys.argv[1])
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
assets = data.get('Assets', [])
# DT_CharacterStat에서 스킬 ID 추출
dt_char_stat = next((dt for dt in assets if dt.get('AssetName') == 'DT_CharacterStat'), None)
stalker_skills = {}
for row in dt_char_stat.get('Rows', []):
row_name = row['RowName']
if row_name in STALKERS:
data_field = row['Data']
stalker_skills[row_name] = {
'defaultSkills': data_field.get('defaultSkills', []),
'subSkill': data_field.get('subSkill', ''),
'ultimateSkill': data_field.get('ultimateSkill', '')
}
# DT_Skill에서 스킬 상세 정보 추출
dt_skill = next((dt for dt in assets if dt.get('AssetName') == 'DT_Skill'), None)
for stalker in STALKERS:
skill_ids = stalker_skills.get(stalker, {})
print(f"\n{'='*120}")
print(f"{stalker.upper()}")
print(f"{'='*120}")
# 기본 스킬들
print(f"\n[기본 스킬]")
for skill_id in skill_ids.get('defaultSkills', []):
skill_row = next((row for row in dt_skill['Rows'] if row['RowName'] == skill_id), None)
if skill_row:
d = skill_row['Data']
print(f"\n {skill_id} - {d.get('name', 'N/A')}")
print(f" 설명: {d.get('simpleDesc', 'N/A')}")
print(f" 타입: {d.get('skillAttackType', 'N/A')}")
print(f" 속성: {d.get('skillElementType', 'N/A')}")
print(f" 피해배율: {d.get('skillDamageRate', 0)}")
print(f" 쿨타임: {d.get('coolTime', 0)}")
print(f" 마나: {d.get('manaCost', 0)}")
# 서브 스킬
sub_skill_id = skill_ids.get('subSkill', '')
if sub_skill_id:
skill_row = next((row for row in dt_skill['Rows'] if row['RowName'] == sub_skill_id), None)
if skill_row:
d = skill_row['Data']
print(f"\n[서브 스킬]")
print(f"\n {sub_skill_id} - {d.get('name', 'N/A')}")
print(f" 설명: {d.get('simpleDesc', 'N/A')}")
print(f" 타입: {d.get('skillAttackType', 'N/A')}")
print(f" 쿨타임: {d.get('coolTime', 0)}")
# 궁극기
ult_skill_id = skill_ids.get('ultimateSkill', '')
if ult_skill_id:
skill_row = next((row for row in dt_skill['Rows'] if row['RowName'] == ult_skill_id), None)
if skill_row:
d = skill_row['Data']
print(f"\n[궁극기]")
print(f"\n {ult_skill_id} - {d.get('name', 'N/A')}")
print(f" 설명: {d.get('simpleDesc', 'N/A')}")
print(f" 타입: {d.get('skillAttackType', 'N/A')}")
if __name__ == "__main__":
main()

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)

View File

@ -0,0 +1,565 @@
#!/usr/bin/env python3
"""
스토커 DPS 시나리오 계산 v2
- 3개 시나리오 계산: 평타, 로테이션 (30초), 버스트 (10초)
- 특수 상황 분석: DoT, 소환체, 패링
"""
import json
import math
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Tuple, Any
from config import (
get_output_dir, STALKERS, STALKER_INFO,
BASE_DAMAGE_FORMULA, DOT_SKILLS, DOT_DAMAGE,
SUMMON_SKILLS, UTILITY_SKILLS, ANALYSIS_BASELINE
)
def load_validated_data(output_dir: Path) -> Dict:
"""validated_data.json 또는 intermediate_data.json 로드"""
validated_file = output_dir / "validated_data.json"
intermediate_file = output_dir / "intermediate_data.json"
if validated_file.exists():
data_file = validated_file
print(f"Using validated_data.json")
elif intermediate_file.exists():
data_file = intermediate_file
print(f"Using intermediate_data.json")
else:
raise FileNotFoundError(f"No data file found in {output_dir}")
with open(data_file, 'r', encoding='utf-8') as f:
return json.load(f)
def calculate_base_damage(stalker_id: str, stats: Dict) -> float:
"""BaseDamage 계산 (Level 20, GearScore 400 기준)"""
role = STALKER_INFO[stalker_id]['role']
# 주 스탯 결정 (실제 높은 스탯 또는 역할 기준)
if stalker_id == 'hilda':
# Hilda: STR 20 → Physical STR
damage_type = 'physical_str'
elif stalker_id == 'urud':
# Urud: DEX 20 → Physical DEX
damage_type = 'physical_dex'
elif stalker_id == 'nave':
# Nave: INT 25 → Magical
damage_type = 'magical'
elif stalker_id == 'baran':
# Baran: STR 25 → Physical STR
damage_type = 'physical_str'
elif stalker_id == 'rio':
# Rio: DEX 25 → Physical DEX
damage_type = 'physical_dex'
elif stalker_id == 'clad':
# Clad: STR 15 (not WIS!) → Support
damage_type = 'support'
elif stalker_id == 'rene':
# Rene: INT 20 → Magical
damage_type = 'magical'
elif stalker_id == 'sinobu':
# Sinobu: DEX 25 → Physical DEX
damage_type = 'physical_dex'
elif stalker_id == 'lian':
# Lian: DEX 20 → Physical DEX
damage_type = 'physical_dex'
elif stalker_id == 'cazimord':
# Cazimord: DEX 25, STR 15 → Physical DEX
damage_type = 'physical_dex'
else:
# Default fallback
damage_type = 'physical_str'
return BASE_DAMAGE_FORMULA[damage_type](stats)
def calculate_basic_attack_dps(stalker_id: str, stalker_data: Dict, base_damage: float) -> Dict:
"""시나리오 1: 평타 DPS 계산"""
basic_attacks = stalker_data.get('basicAttacks', {})
# 첫 번째 무기 타입의 평타 사용 (대부분 한 가지 무기만 사용)
weapon_type = list(basic_attacks.keys())[0] if basic_attacks else None
if not weapon_type:
return {
'dps': 0,
'combo_time': 0,
'total_multiplier': 0,
'attacks': [],
'notes': '평타 데이터 없음'
}
attacks = basic_attacks[weapon_type]
# 총 콤보 시간 및 평타 배율 합계 계산
combo_time = sum(atk['actualDuration'] for atk in attacks)
# attackMultiplier는 AddNormalAttackPer 값 (음수는 감소, 양수는 증가)
# 실제 배율 = 1.0 + (attackMultiplier / 100)
total_multiplier = sum(1.0 + (atk['attackMultiplier'] / 100.0) for atk in attacks)
# 평타 DPS 계산
if combo_time > 0:
basic_dps = (base_damage * total_multiplier) / combo_time
else:
basic_dps = 0
return {
'dps': round(basic_dps, 2),
'combo_time': round(combo_time, 2),
'total_multiplier': round(total_multiplier, 2),
'base_damage': round(base_damage, 2),
'attacks': [
{
'index': atk['index'],
'name': atk['montageName'],
'duration': round(atk['actualDuration'], 2),
'multiplier': round(1.0 + (atk['attackMultiplier'] / 100.0), 2)
}
for atk in attacks
],
'notes': ''
}
def calculate_skill_rotation_dps(stalker_id: str, stalker_data: Dict, base_damage: float, duration: float = 30.0) -> Dict:
"""시나리오 2: 스킬 로테이션 DPS (30초 기본)"""
skills = stalker_data.get('skills', {})
stats = stalker_data['stats']
# 마나 회복 (0.2/초 + 룬 +70% = 0.34/초)
mana_regen_rate = stats.get('manaRegen', 0.2) * (1.0 + ANALYSIS_BASELINE['rune_effect']['cooltime_reduction'])
# 쿨타임 감소 (왜곡 룬 -25%)
cooltime_reduction = ANALYSIS_BASELINE['rune_effect']['cooltime_reduction']
# 공격 스킬만 필터링 (유틸리티 제외, 궁극기 제외)
attack_skills = []
for skill_id, skill in skills.items():
if skill_id in UTILITY_SKILLS:
continue
if skill.get('bIsUltimate', False):
continue
if not skill.get('montageData'):
continue
# 시퀀스 길이 계산 (Ready, Equipment 제외)
sequence_length = sum(
m['actualDuration']
for m in skill['montageData']
if 'Ready' not in m['assetName'] and 'Equipment' not in m['assetName']
)
if sequence_length > 0:
attack_skills.append({
'id': skill_id,
'name': skill['name'],
'damage_rate': skill['skillDamageRate'],
'cooltime': skill['coolTime'] * (1.0 - cooltime_reduction),
'casting_time': skill.get('castingTime', 0),
'sequence_length': sequence_length,
'mana_cost': skill['manaCost'],
'skill_type': skill.get('skillAttackType', 'Physical')
})
# 쿨타임 짧은 순서로 정렬
attack_skills.sort(key=lambda x: x['cooltime'])
# 로테이션 시뮬레이션
current_time = 0.0
current_mana = stats.get('mp', 50)
skill_usage = {s['id']: {'count': 0, 'damage': 0, 'next_available': 0} for s in attack_skills}
basic_attack_time = 0.0
# 평타 DPS (필러로 사용)
basic_result = calculate_basic_attack_dps(stalker_id, stalker_data, base_damage)
basic_dps = basic_result['dps']
# 30초 동안 스킬 사용
time_step = 0.1 # 0.1초 단위로 시뮬레이션
while current_time < duration:
# 현재 사용 가능한 스킬 찾기
skill_used = False
for skill in attack_skills:
# 쿨타임 확인
if skill_usage[skill['id']]['next_available'] > current_time:
continue
# 마나 확인
if current_mana < skill['mana_cost']:
continue
# 스킬 사용
skill_time = skill['casting_time'] + skill['sequence_length']
if current_time + skill_time > duration:
break # 시간 초과
# 피해 계산
if 'Magical' in skill['skill_type']:
skill_damage = base_damage * skill['damage_rate']
else:
skill_damage = base_damage * skill['damage_rate']
skill_usage[skill['id']]['count'] += 1
skill_usage[skill['id']]['damage'] += skill_damage
skill_usage[skill['id']]['next_available'] = current_time + skill_time + skill['cooltime']
current_time += skill_time
current_mana -= skill['mana_cost']
skill_used = True
break
if not skill_used:
# 스킬 사용 불가 시 평타 사용
basic_attack_time += time_step
current_time += time_step
# 마나 회복
current_mana = min(current_mana + mana_regen_rate * time_step, stats.get('mp', 50))
# 총 피해 계산
total_skill_damage = sum(usage['damage'] for usage in skill_usage.values())
basic_damage = basic_dps * basic_attack_time
total_damage = total_skill_damage + basic_damage
rotation_dps = total_damage / duration
return {
'dps': round(rotation_dps, 2),
'duration': duration,
'base_damage': round(base_damage, 2),
'skill_damage': round(total_skill_damage, 2),
'basic_damage': round(basic_damage, 2),
'basic_attack_time': round(basic_attack_time, 2),
'skill_usage': {
skill_id: {
'name': next((s['name'] for s in attack_skills if s['id'] == skill_id), ''),
'count': usage['count'],
'damage': round(usage['damage'], 2)
}
for skill_id, usage in skill_usage.items() if usage['count'] > 0
},
'notes': ''
}
def calculate_burst_dps(stalker_id: str, stalker_data: Dict, base_damage: float, duration: float = 10.0) -> Dict:
"""시나리오 3: 버스트 DPS (10초)"""
skills = stalker_data.get('skills', {})
stats = stalker_data['stats']
# 궁극기 찾기 (유틸리티 제외)
ultimate_skill = None
ultimate_id = None
for skill_id, skill in skills.items():
if skill.get('bIsUltimate', False) and skill_id not in UTILITY_SKILLS:
ultimate_skill = skill
ultimate_id = skill_id
break
# 모든 공격 스킬 (유틸리티 제외)
attack_skills = []
for skill_id, skill in skills.items():
if skill_id in UTILITY_SKILLS:
continue
if skill.get('bIsUltimate', False):
continue
if not skill.get('montageData'):
continue
# 시퀀스 길이 계산
sequence_length = sum(
m['actualDuration']
for m in skill['montageData']
if 'Ready' not in m['assetName'] and 'Equipment' not in m['assetName']
)
if sequence_length > 0:
attack_skills.append({
'id': skill_id,
'name': skill['name'],
'damage_rate': skill['skillDamageRate'],
'casting_time': skill.get('castingTime', 0),
'sequence_length': sequence_length,
'mana_cost': skill['manaCost']
})
# 버스트 시나리오: 궁극기 → 모든 스킬 → 평타
current_time = 0.0
total_damage = 0.0
skill_order = []
# 1. 궁극기 사용 (있는 경우)
if ultimate_skill:
ult_sequence = sum(
m['actualDuration']
for m in ultimate_skill.get('montageData', [])
if 'Ready' not in m['assetName'] and 'Equipment' not in m['assetName']
)
ult_time = ultimate_skill.get('castingTime', 0) + ult_sequence
if current_time + ult_time <= duration:
ult_damage = base_damage * ultimate_skill['skillDamageRate']
total_damage += ult_damage
current_time += ult_time
skill_order.append({
'time': round(current_time, 2),
'skill': ultimate_skill['name'],
'damage': round(ult_damage, 2),
'type': 'ultimate'
})
# 2. 모든 스킬 한 번씩 사용 (피해량 높은 순서)
attack_skills.sort(key=lambda x: x['damage_rate'], reverse=True)
for skill in attack_skills:
skill_time = skill['casting_time'] + skill['sequence_length']
if current_time + skill_time > duration:
continue
skill_damage = base_damage * skill['damage_rate']
total_damage += skill_damage
current_time += skill_time
skill_order.append({
'time': round(current_time, 2),
'skill': skill['name'],
'damage': round(skill_damage, 2),
'type': 'skill'
})
# 3. 남은 시간 평타
remaining_time = duration - current_time
basic_result = calculate_basic_attack_dps(stalker_id, stalker_data, base_damage)
basic_dps = basic_result['dps']
basic_damage = basic_dps * remaining_time
total_damage += basic_damage
burst_dps = total_damage / duration
return {
'dps': round(burst_dps, 2),
'duration': duration,
'base_damage': round(base_damage, 2),
'ultimate_damage': round(skill_order[0]['damage'], 2) if skill_order and skill_order[0]['type'] == 'ultimate' else 0,
'skill_damage': round(sum(s['damage'] for s in skill_order if s['type'] == 'skill'), 2),
'basic_damage': round(basic_damage, 2),
'remaining_time': round(remaining_time, 2),
'skill_order': skill_order,
'has_ultimate': ultimate_skill is not None,
'notes': ''
}
def calculate_dot_dps_by_hp(target_hp_list: List[int] = [100, 500, 1000]) -> Dict:
"""DoT 스킬 DPS (대상 HP별)"""
dot_results = {}
for skill_id, skill_info in DOT_SKILLS.items():
dot_type = skill_info['dot_type']
dot_config = DOT_DAMAGE[dot_type]
dot_results[skill_id] = {
'stalker': skill_info['stalker'],
'name': skill_info['name'],
'dot_type': dot_type,
'description': dot_config['description'],
'dps_by_hp': {}
}
for target_hp in target_hp_list:
if 'rate' in dot_config:
# 퍼센트 피해 (Poison, Burn)
dot_damage = target_hp * dot_config['rate']
dot_dps = dot_damage / dot_config['duration']
else:
# 고정 피해 (Bleed)
dot_damage = dot_config['damage']
dot_dps = dot_damage / dot_config['duration']
dot_results[skill_id]['dps_by_hp'][target_hp] = round(dot_dps, 2)
return dot_results
def calculate_summon_independent_dps(stalker_data: Dict, base_damage: float) -> Dict:
"""소환체 독립 DPS 계산"""
summon_results = {}
for skill_id, summon_info in SUMMON_SKILLS.items():
stalker_id = summon_info['stalker']
# 해당 스토커의 스킬인지 확인
skills = stalker_data.get('skills', {})
if skill_id not in skills:
continue
skill = skills[skill_id]
active_duration = skill.get('activeDuration', 0)
if summon_info['summon'] == 'Ifrit':
# Ifrit: 3개 몽타주 순차 루프 (2.87 + 2.90 + 2.52 = 8.29초 사이클)
# 20초 지속
montage_data = skill.get('montageData', [])
cycle_time = sum(m['actualDuration'] for m in montage_data if 'Ready' not in m['assetName'])
attack_count = (active_duration / cycle_time) * len(montage_data)
# Ifrit 공격: BaseDamage × 1.2
total_damage = base_damage * 1.2 * attack_count
summon_dps = total_damage / active_duration if active_duration > 0 else 0
summon_results[skill_id] = {
'name': summon_info['name'],
'summon': summon_info['summon'],
'active_duration': active_duration,
'cycle_time': round(cycle_time, 2),
'attack_count': round(attack_count, 2),
'dps': round(summon_dps, 2),
'notes': f'{len(montage_data)}개 몽타주 순차 루프'
}
elif summon_info['summon'] == 'Shiva':
# Shiva: 단일 몽타주 2.32초 반복
# 60초 지속
montage_name = summon_info.get('montage', '')
# TODO: 실제 몽타주 시간 찾아서 계산
cycle_time = 2.32 # 임시값
attack_count = active_duration / cycle_time
# Shiva 공격: BaseDamage × 0.8
total_damage = base_damage * 0.8 * attack_count
summon_dps = total_damage / active_duration if active_duration > 0 else 0
summon_results[skill_id] = {
'name': summon_info['name'],
'summon': summon_info['summon'],
'active_duration': active_duration,
'cycle_time': round(cycle_time, 2),
'attack_count': round(attack_count, 2),
'dps': round(summon_dps, 2),
'notes': '단일 몽타주 반복'
}
return summon_results
def save_dps_results_json(all_results: Dict, output_dir: Path) -> None:
"""DPS 계산 결과를 JSON으로 저장 (Claude 분석용)"""
# 정렬된 데이터 준비
scenario1_sorted = sorted(
[(sid, all_results[sid]['scenario1']) for sid in STALKERS if sid in all_results],
key=lambda x: x[1]['dps'],
reverse=True
)
scenario2_sorted = sorted(
[(sid, all_results[sid]['scenario2']) for sid in STALKERS if sid in all_results],
key=lambda x: x[1]['dps'],
reverse=True
)
scenario3_sorted = sorted(
[(sid, all_results[sid]['scenario3']) for sid in STALKERS if sid in all_results],
key=lambda x: x[1]['dps'],
reverse=True
)
# 정렬된 순위 정보 추가
for rank, (stalker_id, _) in enumerate(scenario1_sorted, 1):
all_results[stalker_id]['scenario1']['rank'] = rank
for rank, (stalker_id, _) in enumerate(scenario2_sorted, 1):
all_results[stalker_id]['scenario2']['rank'] = rank
for rank, (stalker_id, _) in enumerate(scenario3_sorted, 1):
all_results[stalker_id]['scenario3']['rank'] = rank
# JSON 저장
output_file = output_dir / "dps_raw_results.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(all_results, f, ensure_ascii=False, indent=2)
print(f"Generated: {output_file}")
def main():
"""메인 실행 함수"""
print("=" * 80)
print("DPS 시나리오 계산 v2")
print("=" * 80)
# 1. 출력 디렉토리 가져오기 (가장 최근 v2 폴더)
output_dir = get_output_dir(create_new=False)
print(f"\nOutput directory: {output_dir}")
# 2. validated_data.json 로드
print("\nLoading validated_data.json...")
validated_data = load_validated_data(output_dir)
print(f"Loaded data for {len(validated_data)} stalkers")
# 3. 각 스토커별 DPS 계산
print("\nCalculating DPS scenarios...")
all_results = {}
for stalker_id in STALKERS:
print(f"\n Processing: {STALKER_INFO[stalker_id]['name']} ({stalker_id})...")
stalker_data = validated_data.get(stalker_id, {})
if not stalker_data:
print(f" WARNING: No data for {stalker_id}, skipping")
continue
stats = stalker_data['stats']['stats']
# BaseDamage 계산
base_damage = calculate_base_damage(stalker_id, stats)
print(f" BaseDamage: {round(base_damage, 2)}")
# 시나리오 1: 평타 DPS
scenario1 = calculate_basic_attack_dps(stalker_id, stalker_data, base_damage)
print(f" Basic Attack DPS: {scenario1['dps']}")
# 시나리오 2: 스킬 로테이션 DPS (30초)
scenario2 = calculate_skill_rotation_dps(stalker_id, stalker_data, base_damage, 30.0)
print(f" Rotation DPS: {scenario2['dps']}")
# 시나리오 3: 버스트 DPS (10초)
scenario3 = calculate_burst_dps(stalker_id, stalker_data, base_damage, 10.0)
print(f" Burst DPS: {scenario3['dps']}")
all_results[stalker_id] = {
'scenario1': scenario1,
'scenario2': scenario2,
'scenario3': scenario3
}
# 소환체 분석 (Rene만)
if stalker_id == 'rene':
summon_analysis = calculate_summon_independent_dps(stalker_data, base_damage)
all_results[stalker_id]['summon_analysis'] = summon_analysis
print(f" Summon analysis complete: {len(summon_analysis)} skills")
# 4. DoT 분석 (전역)
print("\n Calculating DoT DPS...")
dot_analysis = calculate_dot_dps_by_hp([100, 500, 1000])
all_results['dot_analysis'] = dot_analysis
print(f" DoT analysis complete: {len(dot_analysis)} skills")
# 5. JSON 저장 (Claude 분석용)
print("\nSaving DPS results to JSON...")
save_dps_results_json(all_results, output_dir)
print("\n" + "=" * 80)
print("DPS calculation complete!")
print("=" * 80)
print("\nNext step: Run Claude analysis to generate 02_DPS_시나리오_비교분석_v2.md")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,213 @@
#!/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"
# 출력 디렉토리 (Git 버전관리용 고정 경로)
def get_output_dir(create_new: bool = False) -> Path:
"""
출력 디렉토리 가져오기
- Git으로 버전관리하므로 타임스탬프 폴더 생성하지 않음
- 항상 분석결과/ 직접 사용
"""
result_base = PROJECT_ROOT / "분석결과"
result_base.mkdir(parents=True, exist_ok=True)
return result_base
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 제거)',
'SK150301': 'clad - 마석 황금 (보호막)', # 궁극기 - 보호막 스킬
'SK160301': 'rene - 마석 붉은 축제 (흡혈 버프)', # 궁극기 - 흡혈 버프
'SK190301': 'lian - 마석 폭우 (쿨타임 감소)', # 궁극기 - 쿨타임 감소 버프
'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['str'] + 80) * 1.00 # Clad uses STR, not WIS
}
# 특수 궁극기 처리
SPECIAL_ULTIMATE_HANDLING = {
'SK130301': { # 바란 - 일격분쇄
'stalker': 'baran',
'use_an_simplesendevent_time': True, # AN_SimpleSendEvent 시간 사용
'event_tag': 'Ability.Attack.Ready',
'description': 'AN_SimpleSendEvent 시점(1.29초)이 실제 발동 시간, 10초는 최대 홀딩 시간'
}
}
# 검증 기준
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,868 @@
#!/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 노티파이)
- attackStateEndTime 추출 (ANS_AttackState_C 종료 시점)
Returns:
{montage_name: {timing, notifies, attackMultiplier, attackStateEndTime}}
"""
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)
attack_state_end_time = None # ANS_AttackState 종료 시점 (평타용)
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
# ANS_AttackState 종료 시점 = TriggerTime + Duration
trigger_time = notify.get('TriggerTime', 0)
duration = notify.get('Duration', 0)
attack_state_end_time = trigger_time + duration
# 공격 판정 로직 (우선순위)
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)
'attackStateEndTime': attack_state_end_time, # ANS_AttackState 종료 시점 (평타용)
'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 extract_runes(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_Rune에서 룬 데이터 추출
Returns:
{runeId: {runeSet, level, name, desc, attributeModifies, ...}}
"""
print("\n=== DT_Rune 추출 ===")
rune_table = find_table(datatables, 'DT_Rune')
if not rune_table:
print("[WARN] DT_Rune 테이블을 찾을 수 없습니다.")
return {}
runes = {}
for row in rune_table.get('Rows', []):
rune_id = row['RowName']
data = row['Data']
# attributeModifies 파싱
attr_modifies = []
for mod in data.get('attributeModifies', []):
attr = mod.get('attribute', {})
attr_modifies.append({
'attributeName': attr.get('attributeName', ''),
'value': mod.get('value', 0)
})
runes[rune_id] = {
'runeId': rune_id,
'runeSet': data.get('runeSet', ''),
'level': data.get('level', 1),
'name': data.get('runeName', ''),
'desc': format_description(data.get('desc', ''), data.get('descValue', [])),
'descValue': data.get('descValue', []),
'attributeModifies': attr_modifies,
'unlockGold': data.get('unlockGold', 0),
'unlockSkillPoint': data.get('unlockSkillPoint', 0)
}
print(f" [OK] {len(runes)}개 룬 데이터 추출 완료")
return runes
def extract_rune_groups(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_RuneGroup에서 룬 그룹 데이터 추출
Returns:
{groupId: {name, type, coreLine, sub1Line, sub2Line}}
"""
print("\n=== DT_RuneGroup 추출 ===")
rune_group_table = find_table(datatables, 'DT_RuneGroup')
if not rune_group_table:
print("[WARN] DT_RuneGroup 테이블을 찾을 수 없습니다.")
return {}
groups = {}
for row in rune_group_table.get('Rows', []):
group_id = row['RowName']
data = row['Data']
groups[group_id] = {
'groupId': group_id,
'name': data.get('name', ''),
'type': data.get('type', ''),
'coreLine': data.get('coreLine', []),
'sub1Line': data.get('sub1Line', []),
'sub2Line': data.get('sub2Line', [])
}
print(f" [OK] {data.get('name', group_id)}: Core({len(data.get('coreLine', []))}), Sub1({len(data.get('sub1Line', []))}), Sub2({len(data.get('sub2Line', []))})")
return groups
def extract_equipment(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_Equip에서 장비 데이터 추출
Returns:
{equipId: {name, equipSlotType, equipType, rarity, stats, ...}}
"""
print("\n=== DT_Equip 추출 ===")
equip_table = find_table(datatables, 'DT_Equip')
if not equip_table:
print("[WARN] DT_Equip 테이블을 찾을 수 없습니다.")
return {}
equipment = {}
for row in equip_table.get('Rows', []):
equip_id = row['RowName']
data = row['Data']
# stats 파싱
stats = []
for stat in data.get('stats', []):
attr = stat.get('attribute', {})
stats.append({
'attributeName': attr.get('attributeName', ''),
'value': stat.get('value', 0),
'visible': stat.get('visible', False)
})
equipment[equip_id] = {
'equipId': equip_id,
'name': data.get('name', ''),
'desc': data.get('desc', ''),
'equipSlotType': data.get('equipSlotType', ''),
'equipType': data.get('equipType', ''),
'rarity': data.get('rarity', ''),
'price': data.get('price', 0),
'sellPrice': data.get('sellPrice', 0),
'stats': stats,
'armor': data.get('armor', 0)
}
print(f" [OK] {len(equipment)}개 장비 데이터 추출 완료")
return equipment
def extract_float_constants(datatables: List[Dict]) -> Dict[str, float]:
"""
DT_Float에서 기어스코어 공식 상수 추출
Returns:
{constantName: value}
"""
print("\n=== DT_Float (기어스코어 상수) 추출 ===")
float_table = find_table(datatables, 'DT_Float')
if not float_table:
print("[WARN] DT_Float 테이블을 찾을 수 없습니다.")
return {}
constants = {}
gearscore_keys = [
'GearScoreEquipCommon',
'GearScoreEquipUncommon',
'GearScoreEquipRare',
'GearScoreEquipLegendary',
'GearScoreSkillPassive',
'GearScoreSkillPerk'
]
for row in float_table.get('Rows', []):
row_name = row['RowName']
if row_name in gearscore_keys:
constants[row_name] = row['Data'].get('value', 0)
print(f" [OK] {row_name}: {constants[row_name]}")
return constants
def organize_stalker_data(
stalker_stats: Dict,
stalker_abilities: Dict,
all_skills: Dict,
skill_blueprints: Dict,
anim_montages: Dict,
npc_abilities: Dict,
runes: Dict,
rune_groups: Dict,
equipment: Dict,
float_constants: 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} 정보 없음")
# 바란 궁극기 특수 처리: AN_SimpleSendEvent 시점을 castingTime으로 사용
if skill_id == 'SK130301': # 바란 궁극기 '일격분쇄'
for montage_data in skill_data['montageData']:
for notify in montage_data.get('allNotifies', []):
if 'SimpleSendEvent' in notify.get('NotifyClass', ''):
event_tag = notify.get('CustomProperties', {}).get('Event Tag', '')
if 'Ability.Attack.Ready' in event_tag:
trigger_time = notify.get('TriggerTime', 0)
skill_data['castingTime'] = round(trigger_time, 2)
print(f" [INFO] {skill_id}: castingTime 오버라이드 {skill_data['castingTime']}초 (AN_SimpleSendEvent)")
break
# 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]
# 평타는 ANS_AttackState 종료 시점을 우선 사용
# 없으면 actualDuration 폴백
attack_state_end = montage_info.get('attackStateEndTime')
effective_duration = attack_state_end if attack_state_end is not None else montage_info['actualDuration']
basic_attacks[weapon_type].append({
'index': idx + 1,
'montageName': montage_name,
'sequenceLength': montage_info['sequenceLength'],
'rateScale': montage_info['rateScale'],
'actualDuration': montage_info['actualDuration'], # 원본 몽타주 시간
'attackStateEndTime': attack_state_end, # ANS_AttackState 종료 시점
'effectiveDuration': effective_duration, # 실제 평타 시간 (ANS_AttackState 우선)
'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}개 스킬")
# 공통 데이터 추가
organized['_metadata'] = {
'runes': runes,
'runeGroups': rune_groups,
'equipment': equipment,
'gearScoreConstants': float_constants
}
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) # 소환수 데이터
runes = extract_runes(datatables) # 룬 데이터
rune_groups = extract_rune_groups(datatables) # 룬 그룹 데이터
equipment = extract_equipment(datatables) # 장비 데이터
float_constants = extract_float_constants(datatables) # 기어스코어 상수
# 3. 데이터 통합
organized_data = organize_stalker_data(
stalker_stats,
stalker_abilities,
all_skills,
skill_blueprints,
anim_montages,
npc_abilities,
runes,
rune_groups,
equipment,
float_constants
)
# 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,810 @@
#!/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"""# 01. 분석 기초자료 (v2)
## 📌 문서 개요
본 문서는 던전 스토커즈 전투 시스템의 **기초 데이터**를 종합 정리한 자료입니다.
### 구성
1. **분석 전제조건**: 레벨, 기어스코어, 룬 빌드, 장비 스탯 추정
2. **스토커별 기본 데이터**: 10명 스토커의 스탯, 스킬, 평타 정보
3. **특수 시스템 상세**: Parrying, Chain Score, Reload, Charging 등
## 데이터 소스
- `DT_CharacterStat`: 기본 스탯, 스킬 목록
- `DT_CharacterAbility`: 평타 몽타주
- `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 시전시간, 효과)
- `DT_Rune`, `DT_RuneGroup`: 룬 시스템 데이터
- `DT_Equip`, `DT_Float`: 장비 및 기어스코어 상수
- `Blueprint`: 스킬 변수 (ActivationOrderGroup 등)
- `AnimMontage`: 타이밍 및 공격 노티파이, 실제 발사 시점
## 검증 상태
- ✅ 모든 데이터는 최신 JSON에서 추출
- ✅ 교차 검증 완료
- ✅ 출처 명시 (각 데이터 필드별)
---
"""
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']
# effectiveDuration 사용 (ANS_AttackState 종료 시점 우선)
duration = attack.get('effectiveDuration', attack['actualDuration'])
multiplier = attack['attackMultiplier']
mult_display = f"{multiplier:+.1f}" if multiplier != 0 else "0.0"
# 비고: ANS_AttackState 적용 여부 표시
notes = []
tag = get_montage_tag(montage_name)
if tag:
notes.append(tag)
if attack.get('attackStateEndTime') is not None:
notes.append(f"ANS_AttackState: {attack['attackStateEndTime']:.2f}")
note = ", ".join(notes) if notes 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 generate_analysis_prerequisites(data: Dict) -> str:
"""분석 전제조건 섹션 생성"""
md = "## 📋 분석 전제조건\n\n"
# 공통 설정
md += "### 기본 설정\n"
md += f"- **레벨**: {config.ANALYSIS_BASELINE['level']}\n"
md += f"- **기어 스코어**: {config.ANALYSIS_BASELINE['gear_score']}\n"
md += f"- **플레이 스타일**: {config.ANALYSIS_BASELINE['play_style']}\n\n"
# 장비 스탯 추정 (metadata에서 추출)
md += "### 장비 스탯 추정 (기어스코어 400 기준)\n\n"
metadata = data.get('_metadata', {})
gear_constants = metadata.get('gearScoreConstants', {})
md += "**무기** (레벨 20, Rare 등급 기준):\n"
md += "- PhysicalDamage: +65\n"
md += "- MagicalDamage: +65\n\n"
md += "**방어구 3부위** (갑옷, 다리, 액세서리):\n"
md += "- 총 PhysicalDamage: +15\n"
md += "- 총 MagicalDamage: +15\n"
md += "- HP: +120\n"
md += "- Defense: +80\n\n"
md += "**총 장비 보너스**:\n"
md += "- PhysicalDamage: +80\n"
md += "- MagicalDamage: +80\n"
md += "- HP: +120\n"
md += "- Defense: +80\n\n"
# 룬 빌드 설정 (metadata에서 룬 데이터 활용)
md += "### 역할별 최적 룬 빌드\n\n"
runes = metadata.get('runes', {})
rune_groups = metadata.get('runeGroups', {})
# 물리 딜러 빌드 예시 (룬 데이터에서 추출)
md += "#### 물리 딜러 (Hilda, Baran, Rio, Sinobu, Cazimord)\n\n"
md += "**Main: 스킬 그룹 (20xxx)**\n"
md += "- 20101 저주 (조건부 지연 피해)\n"
md += "- 20201 파괴 (+10% 스킬 피해)\n"
md += "- 20301 명상 (+70% 마나 회복)\n\n"
md += "**Sub: 전투 그룹 (10xxx)**\n"
md += "- 10201 분노 (+10% 물리 피해)\n"
md += "- 10103 공략 (+20% 머리 공격 피해)\n\n"
md += "#### 마법 딜러 (Nave, Rene)\n\n"
md += "**Main: 스킬 그룹 (20xxx)**\n"
md += "- 20103 활기 (마나 높을 때 스킬 피해 증가)\n"
md += "- 20202 왜곡 (-25% 쿨타임)\n"
md += "- 20301 명상 (+70% 마나 회복)\n\n"
md += "**Sub: 전투 그룹 (10xxx)**\n"
md += "- 10301 폭풍 (+10% 마법 피해)\n"
md += "- 10103 공략 (+20% 머리 공격 피해)\n\n"
md += "#### 원거리 딜러 (Urud, Lian)\n\n"
md += "**Main: 스킬 그룹 (20xxx)**\n"
md += "- 20101 저주 (지연 피해)\n"
md += "- 20201 파괴 (+10% 스킬 피해)\n"
md += "- 20301 명상 (+70% 마나 회복)\n\n"
md += "**Sub: 전투 그룹 (10xxx)**\n"
md += "- 10201 분노 (+10% 물리 피해)\n"
md += "- 10103 공략 (+20% 머리 공격 피해)\n\n"
md += "#### 서포터 (Clad)\n\n"
md += "**Main: 전투 그룹 (10xxx)**\n"
md += "- 10101 충전 (+30% 궁극기 회복)\n"
md += "- 10202 방패 (+7% 물리 저항)\n"
md += "- 10302 수호 (+7% 마법 저항)\n\n"
md += "**Sub: 보조 그룹 (40xxx)**\n"
md += "- 40201 면역 (물약 사용 시 +20% 저항 20초)\n"
md += "- 40301 효율 (+50% 물약 효과)\n\n"
# 특수 시스템 활용률
md += "### 특수 시스템 활용률\n\n"
md += "**전제**: 최적 플레이 = 100% 활용\n\n"
md += "#### Cazimord - Parrying (흘리기)\n"
md += "- **판정 윈도우**: 0.2초\n"
md += "- **성공 시 효과**:\n"
md += " - 적 피해 무효화\n"
md += " - 자동 반격 (높은 피해)\n"
md += " - **스킬 쿨타임 감소**:\n"
md += " - 섬광(SK170201): -3.8초\n"
md += " - 날개베기(SK170202): -3.8초\n"
md += " - 작열(SK170203): -6.8초\n"
md += "- **활용률 시나리오**: 0% (미사용) vs 100% (완벽 성공)\n\n"
md += "#### Rio - Chain Score\n"
md += "- **최대 스택**: 3\n"
md += "- **효과**: 각 스킬별로 다른 위력 증가\n"
md += "- **충전**: Dropping Attack 성공 시\n"
md += "- **활용률**: 100% (항상 3스택 유지)\n\n"
md += "#### Urud & Lian - Reload\n"
md += "- **탄약**: 6발\n"
md += "- **재장전 시간**: 2.0초\n"
md += "- **활용률**: 100% (탄약 관리 최적화)\n\n"
md += "#### Lian - Charging Bow\n"
md += "- **만충전 데미지**: 1.5배\n"
md += "- **충전 시간**: 레벨당 0.5초 (최대 1.5초)\n"
md += "- **활용률**: 100% (항상 만충전 후 발사)\n\n"
md += "#### Rene - Spirit 소환\n"
md += "- **소환수**: Ifrit, Shiva\n"
md += "- **활용률**: 100% (소환수 항상 활용)\n\n"
md += "#### Sinobu - Shuriken 충전\n"
md += "- **최대 충전**: 3개\n"
md += "- **충전 속도**: 1초/개\n"
md += "- **활용률**: 100% (충전 관리 최적화)\n\n"
md += "---\n\n"
return md
def generate_special_systems(data: Dict) -> str:
"""특수 시스템 상세 분석 섹션 생성"""
md = "## 🔧 특수 시스템 상세\n\n"
md += "### Cazimord - Parrying (흘리기)\n\n"
md += "#### 메커니즘\n"
md += "- **판정 윈도우**: 0.2초\n"
md += "- **패링 성공 시**:\n"
md += " - 적 공격 무효화\n"
md += " - 자동 반격 (높은 피해)\n"
md += " - 스킬 쿨타임 감소\n\n"
md += "#### 쿨타임 감소 효과\n"
md += "| 스킬 | 기본 쿨타임 | 패링 성공 시 감소 | 패링 100% 시 유효 쿨타임 |\n"
md += "|------|-------------|-------------------|------------------------|\n"
# Cazimord 스킬 데이터에서 쿨타임 정보 추출
if 'cazimord' in data:
cazimord = data['cazimord']
skills = cazimord.get('skills', {})
parrying_skills = {
'SK170201': ('섬광', -3.8),
'SK170202': ('날개베기', -3.8),
'SK170203': ('작열', -6.8)
}
for skill_id, (skill_name, reduction) in parrying_skills.items():
if skill_id in skills:
skill = skills[skill_id]
base_cooltime = skill.get('coolTime', 0)
effective_cooltime = max(0, base_cooltime + reduction)
md += f"| {skill_name} | {base_cooltime:.1f}초 | {reduction}초 | {effective_cooltime:.1f}초 |\n"
md += "\n#### DPS 영향\n"
md += "- **패링 0%**: 기본 쿨타임 적용\n"
md += "- **패링 100%**: 쿨타임 감소로 스킬 회전율 증가 → DPS 상승\n\n"
md += "### Rio - Chain Score\n\n"
md += "#### 메커니즘\n"
md += "- **스택 시스템**: 최대 3스택\n"
md += "- **스택 획득**: Dropping Attack 스킬 성공 시 +1\n"
md += "- **효과**: 스킬별로 스택 소모 및 추가 효과 발동\n\n"
md += "#### 스택별 효과\n"
md += "- 각 스킬이 Chain Score 스택을 소모하여 강화\n"
md += "- 스킬마다 다른 위력 증가 배율 적용\n\n"
md += "### Urud & Lian - Reload 시스템\n\n"
md += "#### 메커니즘\n"
md += "- **최대 탄약**: 6발\n"
md += "- **재장전 시간**: 2.0초\n"
md += "- **재장전 중**: 다른 행동 불가 (DPS 손실)\n\n"
md += "#### DPS 영향\n"
md += "- 6발 소진 후 2초 공백 발생\n"
md += "- 최적 플레이: 탄약 관리로 전투 공백 최소화\n\n"
md += "### Lian - Charging Bow\n\n"
md += "#### 메커니즘\n"
md += "- **충전 단계**: 3단계 (0.5초씩)\n"
md += "- **만충전 배율**: 1.5배\n"
md += "- **충전 중**: 이동 속도 감소\n\n"
md += "#### DPS 영향\n"
md += "- 만충전 시 피해량 증가\n"
md += "- 충전 시간 vs 피해량 트레이드오프\n\n"
md += "### Rene - 소환수 시스템\n\n"
md += "#### Ifrit (화염 정령)\n"
if 'rene' in data:
rene = data['rene']
summons = rene.get('summons', {})
if 'Ifrit' in summons:
ifrit = summons['Ifrit']
md += f"- **지속 시간**: {ifrit.get('activeDuration', 0)}\n"
md += f"- **공격 타입**: 근접 화염 공격\n"
md += f"- **독립 DPS**: 계산 필요 (소환수 몽타주 기반)\n\n"
md += "#### Shiva (냉기 정령)\n"
if 'rene' in data and 'Shiva' in rene.get('summons', {}):
shiva = summons.get('Shiva', {})
md += f"- **지속 시간**: {shiva.get('activeDuration', 0)}\n"
md += f"- **공격 타입**: 원거리 냉기 공격\n"
md += f"- **독립 DPS**: 계산 필요 (소환수 몽타주 기반)\n\n"
md += "### Sinobu - Shuriken 충전\n\n"
md += "#### 메커니즘\n"
md += "- **최대 충전**: 3개\n"
md += "- **충전 속도**: 1초/개 (자동)\n"
md += "- **소모**: 특정 스킬 사용 시 1개씩 소모\n\n"
md += "#### DPS 영향\n"
md += "- 충전 관리로 스킬 사용 빈도 조절\n"
md += "- 최적 플레이: 충전 타이밍 고려한 스킬 로테이션\n\n"
md += "---\n\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("[WARN] 검증되지 않은 데이터입니다. 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_analysis_prerequisites(data) # 분석 전제조건 추가
md_content += generate_stalker_overview(data)
md_content += generate_ultimate_overview(data)
md_content += generate_dot_overview(data) # DoT 스킬 종합
# 개별 스토커
stalker_count = 0
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])
stalker_count += 1
# 특수 시스템 상세 추가
md_content += generate_special_systems(data)
# 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 / "01_분석_기초자료_v2.md"
with open(output_file, 'w', encoding='utf-8') as f:
f.write(md_content)
print(f"\n[OK] 문서 생성 완료: {output_file}")
print(f" - 총 {stalker_count}명 스토커 문서 생성")
print(f" - 분석 전제조건 포함")
print(f" - 특수 시스템 상세 포함")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,121 @@
================================================================================
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> v2
================================================================================
[ JSON <20><><EFBFBD><EFBFBD> <20>ε<EFBFBD> ]
Loading: DataTable.json
Loading: Blueprint.json
Loading: AnimMontage.json
=== DT_CharacterStat <20><><EFBFBD><EFBFBD> ===
[OK] Hilda (<28><><EFBFBD><EFBFBD>) (hilda)
[OK] Urud (<28><EFBFBD><ECB8A3>) (urud)
[OK] Nave (<28><><EFBFBD>̺<EFBFBD>) (nave)
[OK] Baran (<28>ٶ<EFBFBD>) (baran)
[OK] Rio (<28><><EFBFBD><EFBFBD>) (rio)
[OK] Clad (Ŭ<><C5AC><EFBFBD><EFBFBD>) (clad)
[OK] Rene (<28><><EFBFBD><EFBFBD>) (rene)
[OK] Sinobu (<28>ó<EFBFBD><C3B3><EFBFBD>) (sinobu)
[OK] Lian (<28><><EFBFBD><EFBFBD>) (lian)
[OK] Cazimord (ī<><C4AB><EFBFBD>𸣵<EFBFBD>) (cazimord)
=== DT_CharacterAbility <20><><EFBFBD><EFBFBD> ===
[OK] hilda: {'weaponShield': 3}
[OK] urud: {'bow': 1}
[OK] nave: {'staff': 2}
[OK] baran: {'twoHandWeapon': 3}
[OK] rio: {'shortSword': 3}
[OK] clad: {'mace': 2}
[OK] rene: {'staff': 3}
[OK] sinobu: {'shortSword': 2}
[OK] lian: {'bow': 1}
[OK] cazimord: {'weaponShield': 3}
=== DT_Skill <20><><EFBFBD><EFBFBD> ===
[OK] <20><> 91<39><31> <20><>ų <20><><EFBFBD><EFBFBD>
- hilda: 8<><38>
- urud: 10<31><30>
- nave: 9<><39>
- baran: 8<><38>
- rio: 8<><38>
- clad: 8<><38>
- rene: 8<><38>
- sinobu: 8<><38>
- lian: 10<31><30>
- cazimord: 14<31><34>
=== GA_Skill Blueprint <20><><EFBFBD><EFBFBD> ===
[OK] <20><> 112<31><32> GA_Skill Blueprint <20><><EFBFBD><EFBFBD>
=== AnimMontage <20><><EFBFBD><EFBFBD> ===
[OK] <20><> 743<34><33> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD> (PC + Summon)
[INFO] <20>޺<EFBFBD> ĵ<><C4B5> <20><><EFBFBD><EFBFBD> <20><>Ÿ<EFBFBD><C5B8>: 9<><39>
- AM_PC_Baran_B_Attack_W01_01: ĵ<><C4B5> 1.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.90<EFBFBD><EFBFBD>)
- AM_PC_Baran_B_Attack_W01_02: ĵ<><C4B5> 1.48<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.93<EFBFBD><EFBFBD>)
- AM_PC_Baran_B_Attack_W01_03: ĵ<><C4B5> 1.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.73<EFBFBD><EFBFBD>)
- AM_PC_Clad_B_Attack_W01_03: ĵ<><C4B5> 1.30<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.73<EFBFBD><EFBFBD>)
- AM_PC_Clad_B_Attack_W01_02: ĵ<><C4B5> 0.97<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 2.27<EFBFBD><EFBFBD>)
- AM_PC_Clad_B_Attack_W01_01: ĵ<><C4B5> 1.14<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 2.00<EFBFBD><EFBFBD>)
- AM_PC_Hilda_B_Attack_W01_01: ĵ<><C4B5> 1.23<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.60<EFBFBD><EFBFBD>)
- AM_PC_Hilda_B_Attack_W01_02: ĵ<><C4B5> 1.23<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.60<EFBFBD><EFBFBD>)
- AM_PC_Hilda_B_Attack_W01_03: ĵ<><C4B5> 1.23<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.37<EFBFBD><EFBFBD>)
[INFO] <20><>ȯ<EFBFBD><C8AF> <20><><EFBFBD><EFBFBD> <20><>Ÿ<EFBFBD><C5B8>: 15<31><35>
- AM_PC_Rene_B_Skill_SummonIfrit: 2.33<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.46<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
- AM_PC_Rene_B_Skill_SummonShiva: 3.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.69<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
- AM_Sum_Elemental_Fire_Attack: 1.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Fire_Attack_Splash: 1.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Fire_Death: 2.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Fire_Shock: 2.87<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.87<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Fire_Attack_N01: 3.67<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.29<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
- AM_Sum_Elemental_Fire_Attack_N02: 3.67<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.29<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
- AM_Sum_Elemental_Fire_Stun: 3.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.50<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Fire_Appear: 1.70<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.70<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Fire_Attack_N03: 3.33<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.70<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Ice_Death: 9.33<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 9.33<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Ice_Attack_N01: 4.63<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.32<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Ice_Appear: 3.40<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.40<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
- AM_Sum_Elemental_Ice_Attack: 3.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
=== DT_NPCAbility <20><><EFBFBD><EFBFBD> ===
[OK] Ifrit (none): 2<><32> <20><>Ÿ<EFBFBD><C5B8>
1. AM_Sum_Elemental_Fire_Attack_N01
2. AM_Sum_Elemental_Fire_Attack_N02
[OK] Ifrit (normal): 1<><31> <20><>Ÿ<EFBFBD><C5B8>
1. AM_Sum_Elemental_Fire_Attack_N03
=== DT_Rune <20><><EFBFBD><EFBFBD> ===
[OK] 190<39><30> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>
=== DT_RuneGroup <20><><EFBFBD><EFBFBD> ===
[OK] <20><><EFBFBD><EFBFBD> <20>׷<EFBFBD>: Core(3), Sub1(2), Sub2(2)
[OK] <20><>ų <20>׷<EFBFBD>: Core(3), Sub1(3), Sub2(2)
[OK] <20><><EFBFBD><EFBFBD> <20>׷<EFBFBD>: Core(3), Sub1(2), Sub2(3)
[OK] <20><><EFBFBD><EFBFBD> <20>׷<EFBFBD>: Core(2), Sub1(2), Sub2(2)
[OK] <20><><EFBFBD><EFBFBD> <20>׷<EFBFBD>: Core(3), Sub1(3), Sub2(3)
=== DT_Equip <20><><EFBFBD><EFBFBD> ===
[OK] 485<38><35> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>
=== DT_Float (<28><><EFBFBD><EFBFBD>ھ<EFBFBD> <20><><EFBFBD><EFBFBD>) <20><><EFBFBD><EFBFBD> ===
[OK] GearScoreEquipCommon: 10
[OK] GearScoreEquipUncommon: 30
[OK] GearScoreEquipRare: 50
[OK] GearScoreEquipLegendary: 100
[OK] GearScoreSkillPassive: 50
[OK] GearScoreSkillPerk: 50
=== <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ===
[OK] Hilda (<28><><EFBFBD><EFBFBD>) (hilda): 5<><35> <20><>ų
[OK] Urud (<28><EFBFBD><ECB8A3>) (urud): 6<><36> <20><>ų
[OK] Nave (<28><><EFBFBD>̺<EFBFBD>) (nave): 5<><35> <20><>ų
[OK] Baran (<28>ٶ<EFBFBD>) (baran): 5<><35> <20><>ų
[OK] Rio (<28><><EFBFBD><EFBFBD>) (rio): 5<><35> <20><>ų
[OK] Clad (Ŭ<><C5AC><EFBFBD><EFBFBD>) (clad): 5<><35> <20><>ų
[OK] Rene (<28><><EFBFBD><EFBFBD>) (rene): 5<><35> <20><>ų
[OK] Sinobu (<28>ó<EFBFBD><C3B3><EFBFBD>) (sinobu): 5<><35> <20><>ų
[OK] Lian (<28><><EFBFBD><EFBFBD>) (lian): 6<><36> <20><>ų
[OK] Cazimord (ī<><C4AB><EFBFBD>𸣵<EFBFBD>) (cazimord): 5<><35> <20><>ų
[OK] <20>߰<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: D:\Work\WorldStalker\DS-<2D><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\<5C>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD>\20251028_031316_v2\intermediate_data.json
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>丮: D:\Work\WorldStalker\DS-<2D><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\<5C>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD>\20251028_031316_v2
- <20><> 11<31><31> <20><><EFBFBD><EFBFBD>Ŀ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>

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