v2 폐기하고 v3로 새출발
This commit is contained in:
1468
legacy/ARCHITECTURE.md
Normal file
1468
legacy/ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load Diff
372
legacy/README.md
Normal file
372
legacy/README.md
Normal 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)
|
||||
1109
legacy/분석결과/01_분석_기초자료_v2.md
Normal file
1109
legacy/분석결과/01_분석_기초자료_v2.md
Normal file
File diff suppressed because it is too large
Load Diff
1135
legacy/분석결과/02_DPS_시나리오_비교분석_v2.md
Normal file
1135
legacy/분석결과/02_DPS_시나리오_비교분석_v2.md
Normal file
File diff suppressed because it is too large
Load Diff
253
legacy/분석결과/20251024_000515/01_요약.md
Normal file
253
legacy/분석결과/20251024_000515/01_요약.md
Normal 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 궁극기 수정 반영, 정확한 스킬 정보 기반)
|
||||
286
legacy/분석결과/20251024_000515/02_분석_전제조건.md
Normal file
286
legacy/분석결과/20251024_000515/02_분석_전제조건.md
Normal 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
|
||||
562
legacy/분석결과/20251024_000515/03_스토커별_기본데이터.md
Normal file
562
legacy/분석결과/20251024_000515/03_스토커별_기본데이터.md
Normal 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 (정정 완료)
|
||||
780
legacy/분석결과/20251024_000515/04_DPS_계산_결과.md
Normal file
780
legacy/분석결과/20251024_000515/04_DPS_계산_결과.md
Normal 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
|
||||
380
legacy/분석결과/20251024_000515/05_카지모르드_밸런스_검증.md
Normal file
380
legacy/분석결과/20251024_000515/05_카지모르드_밸런스_검증.md
Normal 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% 활용)
|
||||
450
legacy/분석결과/20251024_000515/06_유틸리티_평가.md
Normal file
450
legacy/분석결과/20251024_000515/06_유틸리티_평가.md
Normal 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
|
||||
700
legacy/분석결과/20251024_000515/07_역할별_차별화.md
Normal file
700
legacy/분석결과/20251024_000515/07_역할별_차별화.md
Normal 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
|
||||
741
legacy/분석결과/20251024_000515/08_밸런스_티어_및_개선안.md
Normal file
741
legacy/분석결과/20251024_000515/08_밸런스_티어_및_개선안.md
Normal 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번 문서 종합
|
||||
360
legacy/분석결과/20251024_000515/README.md
Normal file
360
legacy/분석결과/20251024_000515/README.md
Normal 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
|
||||
**라이선스**: 내부 사용 전용
|
||||
196
legacy/분석결과/20251027_153101_v2/02_DPS_시나리오_비교분석_v2.md
Normal file
196
legacy/분석결과/20251027_153101_v2/02_DPS_시나리오_비교분석_v2.md
Normal 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
|
||||
909
legacy/분석결과/20251027_153101_v2/03_스토커별_기본데이터_v2.md
Normal file
909
legacy/분석결과/20251027_153101_v2/03_스토커별_기본데이터_v2.md
Normal 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
|
||||
**검증 상태**: 검증 완료 ✅
|
||||
52918
legacy/분석결과/20251027_153101_v2/intermediate_data.json
Normal file
52918
legacy/분석결과/20251027_153101_v2/intermediate_data.json
Normal file
File diff suppressed because it is too large
Load Diff
52918
legacy/분석결과/20251027_153101_v2/validated_data.json
Normal file
52918
legacy/분석결과/20251027_153101_v2/validated_data.json
Normal file
File diff suppressed because it is too large
Load Diff
15
legacy/분석결과/20251027_153101_v2/검증_리포트.md
Normal file
15
legacy/분석결과/20251027_153101_v2/검증_리포트.md
Normal file
@ -0,0 +1,15 @@
|
||||
# 데이터 검증 리포트
|
||||
|
||||
**생성 시각**: 1761546661.7020578
|
||||
|
||||
## 전체 요약
|
||||
|
||||
- ✅ 검증 통과: **109개** 항목
|
||||
- ⚠️ 경고: **0개** 항목
|
||||
- ❌ 실패: **0개** 항목
|
||||
- 📊 데이터 신뢰도: **100.0%**
|
||||
|
||||
## ✅ 통과 항목
|
||||
|
||||
총 109개 항목이 검증을 통과했습니다.
|
||||
|
||||
839
legacy/분석결과/dps_raw_results.json
Normal file
839
legacy/분석결과/dps_raw_results.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74681
legacy/분석결과/intermediate_data.json
Normal file
74681
legacy/분석결과/intermediate_data.json
Normal file
File diff suppressed because it is too large
Load Diff
107
legacy/분석도구/legacy/analyze_character_stats.py
Normal file
107
legacy/분석도구/legacy/analyze_character_stats.py
Normal 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()
|
||||
104
legacy/분석도구/legacy/analyze_ge_blueprints.py
Normal file
104
legacy/분석도구/legacy/analyze_ge_blueprints.py
Normal 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()
|
||||
65
legacy/분석도구/legacy/check_ultimate_effects.py
Normal file
65
legacy/분석도구/legacy/check_ultimate_effects.py
Normal 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()
|
||||
115
legacy/분석도구/legacy/extract_activation_order_groups.py
Normal file
115
legacy/분석도구/legacy/extract_activation_order_groups.py
Normal 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()
|
||||
89
legacy/분석도구/legacy/extract_all_ultimates_detailed.py
Normal file
89
legacy/분석도구/legacy/extract_all_ultimates_detailed.py
Normal 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()
|
||||
106
legacy/분석도구/legacy/extract_ge_from_ultimates.py
Normal file
106
legacy/분석도구/legacy/extract_ge_from_ultimates.py
Normal 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()
|
||||
69
legacy/분석도구/legacy/extract_skill_cancel_windows.py
Normal file
69
legacy/분석도구/legacy/extract_skill_cancel_windows.py
Normal 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()
|
||||
199
legacy/분석도구/legacy/extract_stalker_data.py
Normal file
199
legacy/분석도구/legacy/extract_stalker_data.py
Normal 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()
|
||||
77
legacy/분석도구/legacy/extract_ultimate_skills.py
Normal file
77
legacy/분석도구/legacy/extract_ultimate_skills.py
Normal 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()
|
||||
42
legacy/분석도구/legacy/find_ga_skills.py
Normal file
42
legacy/분석도구/legacy/find_ga_skills.py
Normal 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()
|
||||
86
legacy/분석도구/legacy/verify_skills_detailed.py
Normal file
86
legacy/분석도구/legacy/verify_skills_detailed.py
Normal 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()
|
||||
BIN
legacy/분석도구/v2/__pycache__/config.cpython-313.pyc
Normal file
BIN
legacy/분석도구/v2/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
122
legacy/분석도구/v2/archive/README.md
Normal file
122
legacy/분석도구/v2/archive/README.md
Normal 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
|
||||
81
legacy/분석도구/v2/archive/check_baran_clad_skills.py
Normal file
81
legacy/분석도구/v2/archive/check_baran_clad_skills.py
Normal 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()
|
||||
110
legacy/분석도구/v2/archive/check_bp_vars.py
Normal file
110
legacy/분석도구/v2/archive/check_bp_vars.py
Normal 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}' 없음")
|
||||
120
legacy/분석도구/v2/archive/check_bp_verification.py
Normal file
120
legacy/분석도구/v2/archive/check_bp_verification.py
Normal 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')}")
|
||||
69
legacy/분석도구/v2/archive/check_character_ability.py
Normal file
69
legacy/분석도구/v2/archive/check_character_ability.py
Normal 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()
|
||||
62
legacy/분석도구/v2/archive/check_character_ability2.py
Normal file
62
legacy/분석도구/v2/archive/check_character_ability2.py
Normal 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}")
|
||||
72
legacy/분석도구/v2/archive/check_character_ability3.py
Normal file
72
legacy/분석도구/v2/archive/check_character_ability3.py
Normal 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}")
|
||||
47
legacy/분석도구/v2/archive/check_data.py
Normal file
47
legacy/분석도구/v2/archive/check_data.py
Normal 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]}")
|
||||
38
legacy/분석도구/v2/archive/check_first_asset.py
Normal file
38
legacy/분석도구/v2/archive/check_first_asset.py
Normal 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
|
||||
121
legacy/분석도구/v2/archive/check_improvements.py
Normal file
121
legacy/분석도구/v2/archive/check_improvements.py
Normal 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']}")
|
||||
21
legacy/분석도구/v2/archive/check_json_structure.py
Normal file
21
legacy/분석도구/v2/archive/check_json_structure.py
Normal 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())}")
|
||||
80
legacy/분석도구/v2/archive/check_lian_skills.py
Normal file
80
legacy/분석도구/v2/archive/check_lian_skills.py
Normal 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()
|
||||
89
legacy/분석도구/v2/archive/check_lian_skills2.py
Normal file
89
legacy/분석도구/v2/archive/check_lian_skills2.py
Normal 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()
|
||||
47
legacy/분석도구/v2/archive/check_montage_names.py
Normal file
47
legacy/분석도구/v2/archive/check_montage_names.py
Normal 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
|
||||
63
legacy/분석도구/v2/archive/check_send_event_notify.py
Normal file
63
legacy/분석도구/v2/archive/check_send_event_notify.py
Normal 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()
|
||||
43
legacy/분석도구/v2/archive/check_sk150201.py
Normal file
43
legacy/분석도구/v2/archive/check_sk150201.py
Normal 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()
|
||||
32
legacy/분석도구/v2/archive/check_skill_structure.py
Normal file
32
legacy/분석도구/v2/archive/check_skill_structure.py
Normal 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
|
||||
133
legacy/분석도구/v2/archive/investigate_projectile.py
Normal file
133
legacy/분석도구/v2/archive/investigate_projectile.py
Normal 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}'")
|
||||
60
legacy/분석도구/v2/archive/verify_improvements.py
Normal file
60
legacy/분석도구/v2/archive/verify_improvements.py
Normal 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)
|
||||
143
legacy/분석도구/v2/archive/verify_improvements_v2.3.py
Normal file
143
legacy/분석도구/v2/archive/verify_improvements_v2.3.py
Normal 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)
|
||||
565
legacy/분석도구/v2/calculate_dps_scenarios_v2.py
Normal file
565
legacy/분석도구/v2/calculate_dps_scenarios_v2.py
Normal 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()
|
||||
213
legacy/분석도구/v2/config.py
Normal file
213
legacy/분석도구/v2/config.py
Normal 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': '[장비]'
|
||||
}
|
||||
}
|
||||
868
legacy/분석도구/v2/extract_stalker_data_v2.py
Normal file
868
legacy/분석도구/v2/extract_stalker_data_v2.py
Normal 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()
|
||||
810
legacy/분석도구/v2/generate_stalker_docs_v2.py
Normal file
810
legacy/분석도구/v2/generate_stalker_docs_v2.py
Normal 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()
|
||||
121
legacy/분석도구/v2/output.txt
Normal file
121
legacy/분석도구/v2/output.txt
Normal 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>
|
||||
107
legacy/분석도구/v2/utils/README.md
Normal file
107
legacy/분석도구/v2/utils/README.md
Normal 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
|
||||
25
legacy/분석도구/v2/utils/list_asset_types.py
Normal file
25
legacy/분석도구/v2/utils/list_asset_types.py
Normal 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}")
|
||||
19
legacy/분석도구/v2/utils/list_datatables.py
Normal file
19
legacy/분석도구/v2/utils/list_datatables.py
Normal 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)")
|
||||
356
legacy/분석도구/v2/validate_stalker_data.py
Normal file
356
legacy/분석도구/v2/validate_stalker_data.py
Normal 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()
|
||||
204
legacy/분석도구/v2/장기과제_Blueprint변수검증.md
Normal file
204
legacy/분석도구/v2/장기과제_Blueprint변수검증.md
Normal 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
|
||||
244
legacy/분석도구/v2/정리_보고서.md
Normal file
244
legacy/분석도구/v2/정리_보고서.md
Normal 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
|
||||
Reference in New Issue
Block a user