분석 v2.1

This commit is contained in:
Gnill82
2025-10-28 12:34:12 +09:00
parent ee1900f268
commit a354adf371
17 changed files with 415324 additions and 334641 deletions

View File

@ -722,6 +722,747 @@ if isinstance(val, float):
--- ---
## 📊 12. v2 분석 프로세스 (4단계 파이프라인)
### 12.1 프로세스 개요
v2 분석 시스템은 JSON 원본 데이터에서 최종 밸런스 리포트까지 4단계 파이프라인으로 구성됩니다.
```
[원본 JSON] → [01단계] → [02단계] → [03단계] → [04단계]
기본데이터 DPS계산 역할비교 밸런스티어
```
**출력 구조**:
```
분석결과/YYYYMMDD_HHMMSS_v2/
├── 01_스토커별_기본데이터_v2.md # 01단계 출력
├── 02_DPS_시나리오_비교분석_v2.md # 02단계 출력
├── 03_역할별_차별화_v2.md # 03단계 출력
├── 04_밸런스_티어_및_개선안_v2.md # 04단계 출력
├── intermediate_data.json # 중간 데이터
├── validated_data.json # 검증된 데이터
└── 검증_리포트.md # 검증 리포트
```
### 12.2 01단계: 스토커별 기본 데이터
#### 목적
- JSON 원본에서 10명 스토커의 기본 정보 추출 및 검증
- 평타, 스킬, 소환체 데이터 문서화
#### 입력
- `원본데이터/DataTable.json`
- `원본데이터/Blueprint.json`
- `원본데이터/AnimMontage.json`
#### 출력
- `01_스토커별_기본데이터_v2.md`
- `validated_data.json`
#### 실행 스크립트
```bash
cd 분석도구/v2
python extract_stalker_data_v2.py
python validate_stalker_data.py
python generate_stalker_docs_v2.py
```
#### 핵심 알고리즘
**1. 공격 스킬 판정 (우선순위)**:
```python
# Priority 1: NotifyName 키워드
if any(keyword in notify_name for keyword in ['AttackWithEquip', 'Projectile', 'SkillActive']):
is_attack = True
# Priority 2: CustomProperties.NotifyName
custom_notify_name = custom_props.get('NotifyName', '')
if any(keyword in custom_notify_name for keyword in ATTACK_KEYWORDS):
is_attack = True
# Priority 3: NotifyClass 키워드
if any(keyword in notify_class for keyword in ATTACK_KEYWORDS):
is_attack = True
# Priority 4: SimpleSendEvent Event Tag
if 'SimpleSendEvent' in notify_class:
event_tag = custom_props.get('Event Tag', '')
if 'Event.SkillActivate' in event_tag or 'Event.SpawnProjectile' in event_tag:
is_attack = True
```
**2. 시퀀스 길이 계산**:
```python
def calculate_sequence_length(skill_id, montage_data):
# 1. 키워드 제외 (Ready, Equipment)
# 2. 특정 몽타주 제외 (exclude_montages 설정)
# 3. 인덱스 제외 (exclude_montage_indices 설정)
# 4. 평균 계산 (average_skills 설정)
if skill_id in average_skills:
return sum(durations) / len(durations), True
else:
return sum(durations), False
```
**3. 소환체 공격 사이클**:
```python
# 순차 루프 계산 (1→2→3→1→2→3...)
total_cycle = sum(montage_durations)
cycle_count = active_duration / total_cycle
attack_count = cycle_count * len(montages)
```
#### 검증 체크리스트
- [ ] 10명 스토커 모두 추출됨
- [ ] 모든 스토커 스탯 합계 = 75
- [ ] 궁극기 10개 확인
- [ ] 공격 스킬 vs 유틸리티 분류 정확성
- [ ] 시퀀스 길이 0이 아닌 값
- [ ] 소환체 데이터 (Ifrit, Shiva)
- [ ] DoT 스킬 4개 (Poison, Burn, Bleed)
### 12.3 02단계: DPS 시나리오 비교분석
#### 목적
- 3개 DPS 시나리오 계산 (평타, 로테이션, 버스트)
- 특수 상황 분석 (DoT, 소환체, 패링)
- 신규 스토커 중심 상세 분석
#### 입력
- `validated_data.json` (01단계 출력)
- `config.py` (BaseDamage 계산 설정)
#### 출력
- `02_DPS_시나리오_비교분석_v2.md`
#### 실행 스크립트
```bash
cd 분석도구/v2
python calculate_dps_scenarios_v2.py
```
#### BaseDamage 계산식
**레벨 20, 기어스코어 400 기준**:
```python
# 물리 딜러
Physical_BaseDamage = (주스탯 + 80) × 1.20
# 주스탯: STR or DEX
# 80: 장비 보너스
# 1.20: 룬 효과 (+10% 물리 + +10% 스킬)
# 마법 딜러
Magical_BaseDamage = (INT + 80) × 1.10
# 1.10: 룬 효과 (+10% 마법)
# 탱커/서포터
Support_BaseDamage = (주스탯 + 80) × 1.00
# 생존력 중심 (피해 증가 룬 없음)
```
#### 시나리오 1: 평타 DPS
**목적**: 순수 평타만으로 지속 딜 측정
**계산식**:
```python
평타_DPS = (BaseDamage × 평타배율합계) / 콤보시간
# 예: Rio
# BaseDamage = (25 + 80) × 1.20 = 126
# 평타배율합계 = (1.0 - 0.3) + (1.0 - 0.2) + (1.0 - 0.15) = 2.15
# 콤보시간 = 1.17 + 1.33 + 1.37 = 3.87초
# 평타_DPS = (126 × 2.15) / 3.87 = 69.9
```
**특수 처리**:
```python
# Urud, Lian: Reload
평타_DPS_with_reload = 평타_DPS × (발사횟수 / (발사시간 + reload시간))
# Lian: Charging
평타_DPS_charged = (BaseDamage × 1.5) / (충전시간 + 발사시간)
```
#### 시나리오 2: 스킬 로테이션 DPS (30초)
**목적**: 스킬 + 평타 조합한 실전 DPS
**계산식**:
```python
로테이션_DPS = (30초간_총_피해량) / 30
# 스킬 사용 횟수
스킬_사용횟수 = floor((30 - castingTime) / (coolTime + 시퀀스길이))
# 평타 필러 시간
평타_필러_시간 = 30 - sum(스킬_사용시간)
```
**로테이션 규칙**:
1. 유틸리티 스킬 제외 (isUtility=True)
2. 쿨타임 짧은 순서로 우선 사용
3. 마나 관리: 0.2/초 + 룬 +70% = 0.34/초
4. 스킬 쿨타임 중 평타 사용
**DoT 피해 추가**:
```python
# Poison/Burn: 대상 MaxHP 비례
DoT_피해 = 대상_MaxHP × DoT_rate × (30 / DoT_duration)
# Bleed: 고정 피해
DoT_피해 = 고정피해 × (30 / DoT_duration)
```
**소환체 피해 추가**:
```python
# Ifrit: 20초 지속, 8.29초 사이클
Ifrit_공격횟수 = 20 / 8.29 × 3 = ~7.2
Ifrit_피해 = 7.2 × BaseDamage × 1.2
# Shiva: 60초 지속, 2.32초 사이클
Shiva_공격횟수 = 30 / 2.32 = ~12.9
Shiva_피해 = 12.9 × BaseDamage × 0.8
```
#### 시나리오 3: 버스트 DPS (10초)
**목적**: 궁극기 포함 최대 화력
**계산식**:
```python
버스트_DPS = (궁극기_피해 + 모든_스킬_피해 + 평타_피해) / 10
```
**조건**:
- 모든 스킬 쿨타임 완료 상태
- 마나 제한 무시 (풀 마나 50 + 회복)
- 최적 순서로 스킬 사용
**유틸리티 궁극기 처리**:
```python
# Lian: 폭우 (쿨타임 -50%, 15초)
버스트기간 = 10
스킬_추가사용 = 쿨타임_50%_감소로_인한_추가_발동
# Hilda: 핏빛 달 (공격력 +15, 20초)
버스트기간내_스킬피해 = (BaseDamage + 15) × 스킬배율
```
#### 특수 상황 분석
**1. DoT DPS (대상 HP별)**:
```python
DoT_DPS_table = {
'100HP': {
'Poison': 100 × 0.20 / 5 = 4 DPS,
'Burn': 100 × 0.10 / 3 = 3.33 DPS
},
'500HP': {
'Poison': 500 × 0.20 / 5 = 20 DPS,
'Burn': 500 × 0.10 / 3 = 16.67 DPS
},
'1000HP': {
'Poison': 1000 × 0.20 / 5 = 40 DPS,
'Burn': 1000 × 0.10 / 3 = 33.33 DPS
}
}
```
**2. 소환체 독립 DPS**:
```python
# Ifrit (20초 지속)
Ifrit_DPS = (BaseDamage × 1.2 × 7.2) / 20
# Shiva (60초 지속)
Shiva_DPS = (BaseDamage × 0.8 × 25.9) / 60
```
**3. 패링 시나리오 (Cazimord)**:
```python
# 패링 0%
DPS_no_parry = 기본_로테이션_DPS
# 패링 50% (5회/10회 성공)
쿨감_효과 = 섬광_3.8 + 날개베기_3.8 + 작열_6.8
추가_스킬사용 = 쿨감으로_인한_추가_발동
DPS_50_parry = 기본_DPS + 추가_스킬_DPS
# 패링 100% (10회/10회 성공)
DPS_100_parry = 기본_DPS + (추가_스킬_DPS × 2)
```
#### 출력 구조
**시나리오별 비교표**:
```markdown
## 시나리오 1: 평타 DPS
| 순위 | 스토커 | BaseDamage | 평타 DPS | 특수 처리 |
|------|--------|------------|----------|-----------|
| 1 | Rio | 126 | 69.9 | Chain Score 3스택 |
| ... |
## 시나리오 2: 스킬 로테이션 DPS (30초)
| 순위 | 스토커 | 로테이션 DPS | 주요 스킬 | DoT/소환체 |
|------|--------|--------------|-----------|------------|
| 1 | Cazimord | 221 | 섬광+날개베기+작열 | - |
| ... |
## 시나리오 3: 버스트 DPS (10초)
| 순위 | 스토커 | 버스트 DPS | 궁극기 | 특징 |
|------|--------|------------|--------|------|
| 1 | Cazimord | 256 | 칼날폭풍 (10.0배) | 단일 최강 |
| ... |
```
**신규 스토커 상세 분석** (Cazimord):
```markdown
## 신규 스토커 상세 분석: Cazimord
### 평타 DPS
- 3타 콤보: ...
- 타임라인: 0초 1타 → 1.67초 2타 → 3.57초 3타 → 5.44초 반복
### 30초 로테이션
**타임라인**:
```
0.0초: 작열 시전 (2초 casting + 2.43초)
4.43초: 평타 콤보 시작
9.87초: 섬광 (1.73초)
11.6초: 날개베기 (2.00초)
13.6초: 평타 콤보
...
```
**패링 영향**:
- 패링 0%: 221 DPS
- 패링 50%: 245 DPS (+10.9%)
- 패링 100%: 268 DPS (+21.3%)
### 버스트 DPS (10초)
**궁극기 칼날폭풍**:
- 12연타: 80% × 10회 + 100% × 2회 = 10.0배
- 타임라인: ...
```
#### 검증 체크리스트
- [ ] 10명 스토커 모두 3개 시나리오 계산됨
- [ ] BaseDamage 계산 정확성
- [ ] 평타 배율 합계 정확성
- [ ] 스킬 로테이션 마나 부족 없음
- [ ] DoT 피해 대상 HP별 표시
- [ ] 소환체 공격 횟수 정확성
- [ ] 신규 스토커 상세 타임라인 포함
### 12.4 03단계: 역할별 차별화
#### 목적
- 5개 역할군 비교 (전사, 원거리, 마법사, 암살자, 서포터)
- 동일 역할 내 차별화 포인트 분석
#### 입력
- `02_DPS_시나리오_비교분석_v2.md` (DPS 데이터)
- `validated_data.json` (스킬 데이터)
#### 출력
- `03_역할별_차별화_v2.md`
#### 역할군 분류
| 역할군 | 스토커 | 인원 |
|--------|--------|------|
| **전사** | Hilda, Baran, Cazimord | 3명 |
| **원거리** | Urud, Lian | 2명 |
| **마법사** | Nave, Rene | 2명 |
| **암살자** | Rio, Sinobu | 2명 |
| **서포터** | Clad | 1명 |
#### 분석 항목
**각 역할군마다**:
1. **공통점**: 무기, 공격타입, 룬효과, 평타콤보
2. **스탯 비교**: STR/DEX/INT/CON/WIS, BaseDamage, DPS
3. **스킬 구성 비교**: 쿨타임, 배율, 특수효과
4. **차별화 포인트**: 핵심 시스템, 강점/약점, 플레이스타일
#### 출력 구조
```markdown
## 1. 전사 (Warriors) - 3명 비교
### 공통점
| 항목 | 공통 특성 |
|------|----------|
| **무기 타입** | 근접 무기 |
| **공격 타입** | Physical 피해 |
### 스탯 비교
| 스토커 | STR | DEX | BaseDamage | 평타 DPS | 로테이션 DPS |
|--------|-----|-----|------------|----------|--------------|
| Hilda | 20 | 15 | 120 | 69.9 | 117 |
| Baran | 25 | 10 | 126 | 84.2 | 128 |
| Cazimord | 15 | 25 | 126 | 91.5 | 221 |
### 차별화 포인트
#### Hilda - 방어형 탱커
- **핵심 시스템**: Blocking
- **강점**: 최고 생존력
- **약점**: 낮은 DPS
- **플레이스타일**: ...
#### Baran - CC 특화 전사
...
#### Cazimord - 고숙련도 DPS 전사
...
```
#### 검증 체크리스트
- [ ] 5개 역할군 모두 분석됨
- [ ] 각 역할군 공통점 명시
- [ ] 스탯/DPS 비교표 정확성
- [ ] 차별화 포인트 명확함
- [ ] 신규 스토커 역할 위치 명확
### 12.5 04단계: 밸런스 티어 및 개선안
#### 목적
- 종합 티어 평가 (OP/S+/S/A/B)
- DPS, 유틸리티별 티어
- 밸런스 개선안 제시
#### 입력
- `02_DPS_시나리오_비교분석_v2.md` (DPS 데이터)
- `03_역할별_차별화_v2.md` (역할 분석)
#### 출력
- `04_밸런스_티어_및_개선안_v2.md`
#### 티어 기준
**종합 티어** (DPS + 유틸리티):
- **OP** (Overpowered): 과도한 성능, 즉시 조정 필요
- **S+**: 최상위, 역할 모델
- **S**: 상위, 경쟁력 우수
- **A**: 중상위, 밸런스 양호
- **B**: 중하위, 개선 필요
**평가 지표**:
```python
종합_점수 = (로테이션_DPS × 0.4) + (버스트_DPS × 0.3) + (유틸리티_점수 × 0.3)
# 유틸리티 점수 (0~20점)
유틸리티_점수 = CC점수 + 생존력점수 + 기동성점수 + 팀기여점수
```
#### 출력 구조
```markdown
## 1. 종합 티어표
| 티어 | 스토커 | 로테이션 DPS | 유틸리티 | 주요 강점 | 밸런스 상태 |
|------|--------|--------------|----------|-----------|-------------|
| **OP** | Rio | 268 | 13점 | 압도적 DPS | ⚠️ 너프 필요 |
| **S+** | Cazimord | 221 | 15점 | 버스트 1위 | ✅ 양호 |
| ... |
## 2. DPS 티어별 분석
### OP 티어 (너프 필요)
**Rio**:
- 현재 DPS: 268
- 문제점: 2위보다 +21% 과다
- 개선안:
1. Chain Score 배율 감소 (150% → 100%)
2. 연속 찌르기 쿨타임 증가 (3.5초 → 5초)
3. 예상 DPS: 220 (-18%)
### B 티어 (버프 필요)
**Urud**:
- 현재 DPS: 82
- 문제점: Reload 페널티 과다
- 개선안:
1. 재장전 시간 감소 (2.0초 → 1.5초)
2. 탄약 증가 (6발 → 8발)
3. 예상 DPS: 105 (+28%)
```
#### 검증 체크리스트
- [ ] 10명 모두 티어 배정됨
- [ ] 티어 기준 명확함
- [ ] DPS 격차 분석 정확성
- [ ] 개선안 구체적 (수치 포함)
- [ ] 예상 DPS 재계산됨
### 12.6 전체 프로세스 검증
#### 일관성 체크
- [ ] 01~04단계 스토커 순서 동일
- [ ] 01단계 BaseDamage = 02단계 BaseDamage
- [ ] 02단계 DPS = 03단계 DPS
- [ ] 03단계 분석 = 04단계 티어 근거
#### 데이터 무결성
- [ ] 중간 파일 존재 (intermediate_data.json, validated_data.json)
- [ ] 모든 스킬 ID 일치
- [ ] 소환체/DoT 데이터 누락 없음
#### 문서 품질
- [ ] Markdown 형식 정확성
- [ ] 표 정렬 일관성
- [ ] 계산식 명시
- [ ] 출처 표시
---
## 📊 13. v2.1 업데이트 - 콤보 캔슬 시스템 발견 (2025-10-28)
### 13.1 주요 발견사항
#### 13.1.1 콤보 캔슬 시스템 (Game Changer!)
**발견 배경**:
- AnimMontage.json 분석 중 `ANS_DisableBlockingState_C` 노티파이 발견
- 특정 스토커의 평타 모션에서 조기 캔슬이 가능함을 확인
**노티파이 구조**:
```json
{
"NotifyName": "ANS_DisableBlockingState_C",
"TriggerTime": 2.73,
"Duration": 1.0,
"NotifyType": "NotifyState",
"NotifyStateClass": "ANS_DisableBlockingState_C"
}
```
**캔슬 가능 시점 계산**:
```python
cancellable_time = TriggerTime + Duration
# 예: 2.73 + 1.0 = 3.73초
# 원본 actualDuration: 4.57초
# 캔슬 시간: 3.73초
# 시간 단축: (4.57 - 3.73) / 4.57 = 18.4% ≈ 19%
```
**적용 대상 스토커**:
| 스토커 | 무기 타입 | 원본 시간 | 캔슬 시간 | 단축율 | DPS 변화 |
|--------|-----------|-----------|-----------|--------|----------|
| **클라드** | oneHandWeapon (Mace) | 4.17초 | 1.84초 | **56%** 🔥 | 52.9 → 125.5 (+137%) |
| **힐다** | weaponShield | 4.57초 | 3.69초 | 19% | 87.3 → 107.3 (+23%) |
| **바란** | twoHandWeapon | 5.53초 | 4.48초 | 19% | 79.0 → 90.4 (+14%) |
**영향도 분석**:
- **클라드**: 서포터임에도 **평타 DPS 1위** 달성 (125.5)
- **힐다**: 탱커 중 최고 DPS 달성 (107.3)
- **바란**: 중상위권으로 상승 (90.4)
#### 13.1.2 바란 궁극기 시전시간 정정
**문제점**:
- DT_Skill에서 castingTime: 10초로 표기
- 실제로는 즉발이 아님
**해결**:
AnimMontage.json의 `AN_SimpleSendEvent_C` 노티파이 확인
```json
{
"NotifyClass": "AN_SimpleSendEvent_C",
"TriggerTime": 1.2927,
"CustomProperties": {
"Event Tag": "(TagName=\"Ability.Attack.Ready\")"
}
}
```
**정정 내용**:
- **실제 시전시간**: 1.29초 (AN_SimpleSendEvent TriggerTime)
- **10초의 의미**: 최대 홀딩 시간 (대검을 들고 있으면서 타이밍 조절 가능)
- **DPS 영향**: 버스트 DPS 128.5 → 123.3 (-4%)
**스크립트 자동 처리**:
```python
# extract_stalker_data_v2.py (line 669-679)
if skill_id == 'SK130301': # 바란 궁극기
for montage_data in skill_data['montageData']:
for notify in montage_data.get('allNotifies', []):
if 'SimpleSendEvent' in notify.get('NotifyClass', ''):
event_tag = notify.get('CustomProperties', {}).get('Event Tag', '')
if 'Ability.Attack.Ready' in event_tag:
trigger_time = notify.get('TriggerTime', 0)
skill_data['castingTime'] = round(trigger_time, 2)
break
```
#### 13.1.3 레네 궁극기 실전 필수화
**배경**:
- 초기 분석: 궁극기 제외 (순수 DPS 우선)
- 실제 플레이: 흡혈 50% 효과로 생존 필수
**궁극기 효과**:
- 마석 '붉은 축제' (SK160301)
- 시전시간: 1.5초
- 지속시간: 20초
- 효과: 자신과 아군 모든 공격에 흡혈 50%
**DPS 트레이드오프**:
- **이전 (궁극기 제외)**: 186.4 DPS (15초 버스트 1위)
- **현재 (궁극기 포함)**: 136.7 DPS (15초 버스트 4위)
- **감소율**: -26.6%
- **보상**: 생존력 확보 (실전 필수)
### 13.2 버스트 시나리오 확대 (10초 → 15초)
**변경 이유**:
1. 궁극기 시전시간 포함 시 10초 부족
2. 대부분 궁극기 지속시간 15초 이상
3. 실전 버스트 상황에 더 부합
**새로운 버스트 DPS 순위** (15초):
| 순위 | 스토커 | 15초 DPS | 궁극기 | 주요 변화 |
|------|--------|----------|--------|----------|
| 1 | 카지모르드 | 165.1 | ✅ | 2위 → 1위 (Parrying + 궁극기) |
| 2 | 리오 | 146.9 | ✅ | 변동 없음 |
| 3 | 시노부 | 142.7 | ❌ | 변동 없음 (궁극기 제외) |
| 4 | **레네** | 136.7 | ✅ | **1위 → 4위** (흡혈 생존력) |
| 5 | 클라드 | 125.4 | ❌ | 변동 없음 (콤보 캔슬) |
| 6 | **바란** | 123.3 | ✅ | **5위 → 6위** (시전시간 정정) |
### 13.3 config.py 업데이트 내용
**추가된 설정**:
```python
# 콤보 캔슬 시스템 (v2.1)
COMBO_CANCEL_STALKERS = {
'hilda': {
'weapons': ['weaponShield'],
'patterns': ['AM_PC_Hilda_B_Attack_W01_'],
'time_reduction': 0.19, # 19% 시간 단축
'description': '3타 콤보 캔슬 (4.57s → 3.69s)'
},
'baran': {
'weapons': ['twoHandWeapon'],
'patterns': ['AM_PC_Baran_B_Attack_W01_'],
'time_reduction': 0.19,
'description': '평타 콤보 캔슬 (5.53s → 4.48s)'
},
'clad': {
'weapons': ['oneHandWeapon'],
'patterns': ['AM_PC_Clad_Base_Attack_Mace'],
'time_reduction': 0.56, # 56% 시간 단축 (극적!)
'description': '평타 콤보 캔슬 (4.17s → 1.84s)'
}
}
# 특수 궁극기 처리 (v2.1)
SPECIAL_ULTIMATE_HANDLING = {
'SK130301': { # 바란 - 일격분쇄
'stalker': 'baran',
'use_an_simplesendevent_time': True,
'event_tag': 'Ability.Attack.Ready',
'description': 'AN_SimpleSendEvent 시점(1.29초)이 실제 발동 시간'
}
}
```
### 13.4 extract_stalker_data_v2.py 업데이트
**콤보 캔슬 추출 로직** (line 277-415):
```python
def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
# 콤보 캔슬 적용 대상 스토커 및 패턴
CANCEL_TARGETS = {
'hilda': ['AM_PC_Hilda_B_Attack_W01_'],
'baran': ['AM_PC_Baran_B_Attack_W01_'],
'clad': ['AM_PC_Clad_Base_Attack_Mace']
}
for montage in pc_montages:
# 콤보 캔슬 적용 대상 판별
is_cancel_target = False
for stalker_name, patterns in CANCEL_TARGETS.items():
for pattern in patterns:
if pattern in asset_name:
is_cancel_target = True
break
# ANS_DisableBlockingState_C에서 캔슬 시간 추출
if is_cancel_target and 'ANS_DisableBlockingState' in notify_state_class:
trigger_time = notify.get('TriggerTime', 0)
duration = notify.get('Duration', 0)
cancellable_time = trigger_time + duration
```
**바란 궁극기 특수 처리** (line 669-679):
```python
# 바란 궁극기 특수 처리: AN_SimpleSendEvent 시점을 castingTime으로 사용
if skill_id == 'SK130301': # 바란 궁극기 '일격분쇄'
for montage_data in skill_data['montageData']:
for notify in montage_data.get('allNotifies', []):
if 'SimpleSendEvent' in notify.get('NotifyClass', ''):
event_tag = notify.get('CustomProperties', {}).get('Event Tag', '')
if 'Ability.Attack.Ready' in event_tag:
trigger_time = notify.get('TriggerTime', 0)
skill_data['castingTime'] = round(trigger_time, 2)
print(f" [INFO] {skill_id}: castingTime 오버라이드 {skill_data['castingTime']}초")
break
```
### 13.5 02단계 문서 업데이트 내용
**시나리오 1 - 평타 DPS 순위 변화**:
```markdown
| 순위 | 스토커 | Raw DPS | 특징 |
|------|--------|---------|------|
| 1 | **클라드** | **125.5** | ⚡ 콤보 캔슬 (+137% DPS!) |
| 2 | **힐다** | **107.3** | ⚡ 콤보 캔슬 (+23%) |
| 3 | 시노부 | 97.83 | 표창 충전 시스템 |
| 4 | **바란** | **90.4** | ⚡ 콤보 캔슬 (+14%) |
```
**시나리오 2 - 30초 로테이션 변화**:
- 클라드: 60.1 → 133.6 DPS (+122%, **7위 → 3위**)
- 힐다: 92.1 → 114.1 DPS (+24%)
- 바란: 97.9 → 111.4 DPS (+14%)
**시나리오 3 - 15초 버스트 (신설)**:
- 기존 10초 → 15초로 확대
- 궁극기 사용 정책 명확화:
- 기본: 0초 시점 사용
- 예외: 클라드/시노부 (방어 궁극기 제외)
- 특수: 카지모르드 (작열 → 섬광 → 궁극기)
**중간 결론 섹션 신설**:
- DPS 기준 종합 티어표 (3개 시나리오 통합 평가)
- 밸런스 개선 제안 (C/B티어 수치 조정안)
### 13.6 검증 체크리스트
**v2.1 검증 항목**:
- [x] 콤보 캔슬 시스템 config.py 추가
- [x] 바란 궁극기 특수 처리 스크립트 반영
- [x] extract_stalker_data_v2.py 업데이트
- [x] 01 문서 바란 궁극기 시전시간 정정
- [x] 02 문서 3개 시나리오 콤보 캔슬 반영
- [x] 02 문서 15초 버스트 시나리오 재작성
- [x] 02 문서 중간 결론 섹션 작성
- [x] 종합 티어표 3개 시나리오 통합 평가
- [x] 밸런스 개선 제안 (리안, 우르드, 네이브)
---
**작성자**: AI-assisted Analysis Team **작성자**: AI-assisted Analysis Team
**최종 업데이트**: 2025-10-27 **최종 업데이트**: 2025-10-28
**버전**: 1.0 **버전**: 2.1

152
README.md
View File

@ -72,26 +72,126 @@ JSON Files → Claude Code → Analysis Document
분석 결과를 검토하고 최종 문서를 생성합니다. 분석 결과를 검토하고 최종 문서를 생성합니다.
---
## 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-전투분석_저장소/ DS-전투분석_저장소/
├── README.md # 본 문서 ├── README.md # 본 문서
├── ARCHITECTURE.md # 기술 아키텍처 (v2 프로세스 상세)
├── 분석결과/ # 분석 결과물 ├── 분석결과/ # 분석 결과물
── 20251023/ # 날짜별 분석 ── 20251024_000515/ # 기존 분석 (참고용)
└── DS-전투시스템_종합분석.md └── 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 원본 데이터 (샘플) ├── 원본데이터/ # JSON 원본 데이터
── 20251023/ ── DataTable.json
├── 샘플_DataTable.json ├── AnimMontage.json
├── 샘플_AnimMontage.json ├── Blueprint.json
└── 샘플_Blueprint.json └── CurveTable.json
└── 분석도구/ # Python 분석 스크립트 └── 분석도구/ # Python 분석 스크립트
├── extract_skill_cancel_windows.py ├── v2/ # v2 자동화 도구
├── analyze_character_stats.py │ ├── extract_stalker_data_v2.py
└── extract_activation_order_groups.py │ ├── validate_stalker_data.py
│ ├── generate_stalker_docs_v2.py
│ ├── calculate_dps_scenarios_v2.py # 개발 예정
│ └── config.py
└── utils/ # 유틸리티 스크립트
└── ...
``` ```
## 정기 분석 수행 가이드 ## 정기 분석 수행 가이드
@ -217,14 +317,15 @@ Hilda:
## 최신 분석 결과 ## 최신 분석 결과
**날짜**: 2025-10-23 **날짜**: 2025-10-27 (v2.1)
**분석 문서**: [분석결과/20251023/DS-전투시스템_종합분석.md](분석결과/20251023/DS-전투시스템_종합분석.md) **분석 문서**: `분석결과/20251027_200151_v2/`
**주요 발견**: **주요 발견 (v2.1)**:
- **공격 속도 1위**: Rio (3.867s) - **평타 DPS 1위**: 클라드 (125.5) - 콤보 캔슬로 137% 증가!
- **가장 높은 피해**: Baran, Clad (평타 +30~50%) - **30초 로테이션 1위**: 레네 (158.88) - 소환수 독립 DPS
- **궁극기 보유**: Nave, Baran, Sinobu, Cazimord (4명만) - **15초 버스트 1위**: 카지모르드 (165.1) - Parrying + 궁극기
- **가장 다양한 우선순위**: Cazimord (Group 0, 2, 3, 4, 9 모두 사용) - **종합 S티어**: 시노부, 레네 (모든 시나리오 안정적 상위권)
- **밸런스 개선 필요**: 리안, 우르드 (재장전/충전 페널티 과다)
## 기술 스택 ## 기술 스택
@ -252,5 +353,20 @@ Hilda:
--- ---
**마지막 업데이트**: 2025-10-23 **마지막 업데이트**: 2025-10-28 (v2.1)
**담당자**: AI-assisted Analysis Team **담당자**: 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)

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -136,6 +136,9 @@ UTILITY_SKILLS = {
'SK130101': 'baran - 무기 막기', 'SK130101': 'baran - 무기 막기',
'SK150206': 'clad - 치유', 'SK150206': 'clad - 치유',
'SK150202': 'clad - 신성한 빛 (DOT 제거)', 'SK150202': 'clad - 신성한 빛 (DOT 제거)',
'SK150301': 'clad - 마석 황금 (보호막)', # 궁극기 - 보호막 스킬
'SK160301': 'rene - 마석 붉은 축제 (흡혈 버프)', # 궁극기 - 흡혈 버프
'SK190301': 'lian - 마석 폭우 (쿨타임 감소)', # 궁극기 - 쿨타임 감소 버프
'SK180205': 'sinobu - 바꿔치기 (피격 시 효과)', 'SK180205': 'sinobu - 바꿔치기 (피격 시 효과)',
'SK180206': 'sinobu - 인술 칠흑안개', 'SK180206': 'sinobu - 인술 칠흑안개',
'SK190209': 'lian - 재장전', # 재장전 'SK190209': 'lian - 재장전', # 재장전
@ -175,7 +178,40 @@ BASE_DAMAGE_FORMULA = {
'physical_str': lambda stats: (stats['str'] + 80) * 1.20, 'physical_str': lambda stats: (stats['str'] + 80) * 1.20,
'physical_dex': lambda stats: (stats['dex'] + 80) * 1.20, 'physical_dex': lambda stats: (stats['dex'] + 80) * 1.20,
'magical': lambda stats: (stats['int'] + 80) * 1.10, 'magical': lambda stats: (stats['int'] + 80) * 1.10,
'support': lambda stats: (stats.get('wis', stats.get('con', 0)) + 80) * 1.00 'support': lambda stats: (stats['str'] + 80) * 1.00 # Clad uses STR, not WIS
}
# 콤보 캔슬 시스템 (v2.1)
# ANS_DisableBlockingState_C 노티파이로 조기 캔슬 가능
COMBO_CANCEL_STALKERS = {
'hilda': {
'weapons': ['weaponShield'], # 방패+무기
'patterns': ['AM_PC_Hilda_B_Attack_W01_'], # 평타 패턴
'time_reduction': 0.19, # 19% 시간 단축
'description': '3타 콤보 캔슬 (4.57s → 3.69s)'
},
'baran': {
'weapons': ['twoHandWeapon'], # 양손 무기
'patterns': ['AM_PC_Baran_B_Attack_W01_'],
'time_reduction': 0.19, # 19% 시간 단축
'description': '평타 콤보 캔슬 (5.53s → 4.48s)'
},
'clad': {
'weapons': ['oneHandWeapon'], # 한손 무기 (mace)
'patterns': ['AM_PC_Clad_Base_Attack_Mace'],
'time_reduction': 0.56, # 56% 시간 단축 (극적!)
'description': '평타 콤보 캔슬 (4.17s → 1.84s)'
}
}
# 특수 궁극기 처리 (v2.1)
SPECIAL_ULTIMATE_HANDLING = {
'SK130301': { # 바란 - 일격분쇄
'stalker': 'baran',
'use_an_simplesendevent_time': True, # AN_SimpleSendEvent 시간 사용
'event_tag': 'Ability.Attack.Ready',
'description': 'AN_SimpleSendEvent 시점(1.29초)이 실제 발동 시간, 10초는 최대 홀딩 시간'
}
} }
# 검증 기준 # 검증 기준

View File

@ -278,21 +278,40 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
""" """
AnimMontage.json에서 몽타주 타이밍 및 노티파이 추출 AnimMontage.json에서 몽타주 타이밍 및 노티파이 추출
- AddNormalAttackPer 추출 (ANS_AttackState_C 노티파이) - AddNormalAttackPer 추출 (ANS_AttackState_C 노티파이)
- cancellableTime 추출 (ANS_DisableBlockingState_C 노티파이)
Returns: Returns:
{montage_name: {timing, notifies, attackMultiplier}} {montage_name: {timing, notifies, attackMultiplier, cancellableTime}}
""" """
print("\n=== AnimMontage 추출 ===") print("\n=== AnimMontage 추출 ===")
all_montages = {} all_montages = {}
pc_montages = [m for m in montages if 'AM_PC_' in m.get('AssetName', '') or 'AM_Sum_' in m.get('AssetName', '')] pc_montages = [m for m in montages if 'AM_PC_' in m.get('AssetName', '') or 'AM_Sum_' in m.get('AssetName', '')]
# 콤보 캔슬 적용 대상 스토커 및 패턴 (평타만 해당)
CANCEL_TARGETS = {
'hilda': ['AM_PC_Hilda_B_Attack_W01_'], # weaponShield
'baran': ['AM_PC_Baran_B_Attack_W01_'], # twoHandWeapon
'clad': ['AM_PC_Clad_Base_Attack_Mace'] # oneHandWeapon (mace) - 특수 패턴
}
for montage in pc_montages: for montage in pc_montages:
asset_name = montage['AssetName'] asset_name = montage['AssetName']
# 공격 노티파이 추출 # 공격 노티파이 추출
attack_notifies = [] attack_notifies = []
attack_multiplier = 0.0 # AddNormalAttackPer (기본값 0) attack_multiplier = 0.0 # AddNormalAttackPer (기본값 0)
cancellable_time = None # 콤보 캔슬 가능 시간 (기본값 None)
# 콤보 캔슬 적용 대상 판별
is_cancel_target = False
for stalker_name, patterns in CANCEL_TARGETS.items():
for pattern in patterns:
if pattern in asset_name:
is_cancel_target = True
break
if is_cancel_target:
break
for notify in montage.get('AnimNotifies', []): for notify in montage.get('AnimNotifies', []):
notify_class = notify.get('NotifyClass', '') notify_class = notify.get('NotifyClass', '')
@ -308,6 +327,12 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
except (ValueError, TypeError): except (ValueError, TypeError):
attack_multiplier = 0.0 attack_multiplier = 0.0
# ANS_DisableBlockingState_C에서 콤보 캔슬 시간 추출 (적용 대상만)
if is_cancel_target and 'ANS_DisableBlockingState' in notify_state_class:
trigger_time = notify.get('TriggerTime', 0)
duration = notify.get('Duration', 0)
cancellable_time = trigger_time + duration
# 공격 판정 로직 (우선순위) # 공격 판정 로직 (우선순위)
is_attack_notify = False is_attack_notify = False
@ -354,6 +379,7 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
'sequenceLength': seq_len, 'sequenceLength': seq_len,
'rateScale': rate_scale, 'rateScale': rate_scale,
'actualDuration': actual_duration, # 시퀀스 길이 (SequenceLength / RateScale) 'actualDuration': actual_duration, # 시퀀스 길이 (SequenceLength / RateScale)
'cancellableTime': cancellable_time, # 콤보 캔슬 가능 시간 (해당되는 경우만)
'attackMultiplier': attack_multiplier, # AddNormalAttackPer 'attackMultiplier': attack_multiplier, # AddNormalAttackPer
'sections': montage.get('Sections', []), 'sections': montage.get('Sections', []),
'numSections': montage.get('NumSections', 0), 'numSections': montage.get('NumSections', 0),
@ -367,6 +393,15 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
print(f" [OK] 총 {len(all_montages)}개 몽타주 추출 (PC + Summon)") print(f" [OK] 총 {len(all_montages)}개 몽타주 추출 (PC + Summon)")
# 콤보 캔슬 적용된 몽타주 확인
cancel_montages = [(name, data['cancellableTime'], data['actualDuration'])
for name, data in all_montages.items()
if data.get('cancellableTime') is not None]
if cancel_montages:
print(f" [INFO] 콤보 캔슬 적용 몽타주: {len(cancel_montages)}")
for name, cancel_time, actual_time in cancel_montages:
print(f" - {name}: 캔슬 {cancel_time:.2f}초 (원본 {actual_time:.2f}초)")
# 소환수 몽타주 확인 # 소환수 몽타주 확인
summon_montages = [m for m in all_montages.keys() if 'Summon' in m or 'Sum_' in m] summon_montages = [m for m in all_montages.keys() if 'Summon' in m or 'Sum_' in m]
if summon_montages: if summon_montages:
@ -419,13 +454,165 @@ def extract_npc_abilities(datatables: List[Dict]) -> Dict[str, Dict]:
return npc_abilities 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( def organize_stalker_data(
stalker_stats: Dict, stalker_stats: Dict,
stalker_abilities: Dict, stalker_abilities: Dict,
all_skills: Dict, all_skills: Dict,
skill_blueprints: Dict, skill_blueprints: Dict,
anim_montages: Dict, anim_montages: Dict,
npc_abilities: Dict npc_abilities: Dict,
runes: Dict,
rune_groups: Dict,
equipment: Dict,
float_constants: Dict
) -> Dict[str, Dict]: ) -> Dict[str, Dict]:
""" """
스토커별로 모든 데이터를 통합 정리 스토커별로 모든 데이터를 통합 정리
@ -479,6 +666,18 @@ def organize_stalker_data(
else: else:
print(f" [WARN] {skill_id}: 몽타주 {montage_name} 정보 없음") 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 스킬 체크 # DoT 스킬 체크
skill_data['isDot'] = skill_id in config.DOT_SKILLS skill_data['isDot'] = skill_id in config.DOT_SKILLS
@ -540,6 +739,7 @@ def organize_stalker_data(
'sequenceLength': montage_info['sequenceLength'], 'sequenceLength': montage_info['sequenceLength'],
'rateScale': montage_info['rateScale'], 'rateScale': montage_info['rateScale'],
'actualDuration': montage_info['actualDuration'], 'actualDuration': montage_info['actualDuration'],
'cancellableTime': montage_info.get('cancellableTime'), # 콤보 캔슬 시간 (해당되는 경우)
'attackMultiplier': montage_info['attackMultiplier'], 'attackMultiplier': montage_info['attackMultiplier'],
'hasAttack': montage_info['hasAttack'] 'hasAttack': montage_info['hasAttack']
}) })
@ -583,6 +783,14 @@ def organize_stalker_data(
skill_count = len(skills) skill_count = len(skills)
print(f" [OK] {stats['name']} ({stalker_id}): {skill_count}개 스킬") print(f" [OK] {stats['name']} ({stalker_id}): {skill_count}개 스킬")
# 공통 데이터 추가
organized['_metadata'] = {
'runes': runes,
'runeGroups': rune_groups,
'equipment': equipment,
'gearScoreConstants': float_constants
}
return organized return organized
def is_utility_skill(skill_data: Dict) -> bool: def is_utility_skill(skill_data: Dict) -> bool:
@ -643,6 +851,10 @@ def main():
skill_blueprints = extract_skill_blueprints(blueprints) skill_blueprints = extract_skill_blueprints(blueprints)
anim_montages = extract_anim_montages(montages) anim_montages = extract_anim_montages(montages)
npc_abilities = extract_npc_abilities(datatables) # 소환수 데이터 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. 데이터 통합 # 3. 데이터 통합
organized_data = organize_stalker_data( organized_data = organize_stalker_data(
@ -651,7 +863,11 @@ def main():
all_skills, all_skills,
skill_blueprints, skill_blueprints,
anim_montages, anim_montages,
npc_abilities npc_abilities,
runes,
rune_groups,
equipment,
float_constants
) )
# 4. 결과 저장 (새 디렉토리 생성) # 4. 결과 저장 (새 디렉토리 생성)

View File

@ -18,25 +18,31 @@ import config
def generate_header() -> str: def generate_header() -> str:
"""문서 헤더 생성""" """문서 헤더 생성"""
return f"""# 03. 스토커별 기본 데이터 (v2) return f"""# 01. 분석 기초자료 (v2)
## 📌 문서 개요
본 문서는 던전 스토커즈 전투 시스템의 **기초 데이터**를 종합 정리한 자료입니다.
### 구성
1. **분석 전제조건**: 레벨, 기어스코어, 룬 빌드, 장비 스탯 추정
2. **스토커별 기본 데이터**: 10명 스토커의 스탯, 스킬, 평타 정보
3. **특수 시스템 상세**: Parrying, Chain Score, Reload, Charging 등
## 데이터 소스 ## 데이터 소스
- `DT_CharacterStat`: 기본 스탯, 스킬 목록 - `DT_CharacterStat`: 기본 스탯, 스킬 목록
- `DT_CharacterAbility`: 평타 몽타주 - `DT_CharacterAbility`: 평타 몽타주
- `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 시전시간, 효과) - `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 시전시간, 효과)
- `DT_Rune`, `DT_RuneGroup`: 룬 시스템 데이터
- `DT_Equip`, `DT_Float`: 장비 및 기어스코어 상수
- `Blueprint`: 스킬 변수 (ActivationOrderGroup 등) - `Blueprint`: 스킬 변수 (ActivationOrderGroup 등)
- `AnimMontage`: 타이밍 및 공격 노티파이, 실제 발사 시점 - `AnimMontage`: 타이밍 및 공격 노티파이, 실제 발사 시점
## 검증 상태 ## 검증 상태
- ✅ 모든 데이터는 최신 JSON (2025-10-24 15:58:55)에서 추출 - ✅ 모든 데이터는 최신 JSON에서 추출
- ✅ 교차 검증 완료 - ✅ 교차 검증 완료
- ✅ 출처 명시 (각 데이터 필드별) - ✅ 출처 명시 (각 데이터 필드별)
## DPS 계산 시 고려사항
- **시전시간**: 스킬 사용 시 시전시간(CastingTime)이 추가됨
- **실제 공격 시점**: 원거리 스킬(우르드, 리안)의 경우 몽타주 시간보다 빠르게 공격 가능
- **DoT 데미지**: DoT(Damage over Time) 스킬은 대상 HP에 비례하여 지속 피해 발생 (구체적 계산은 다음 챕터 참조)
--- ---
""" """
@ -511,6 +517,223 @@ def generate_skill_entry(skill: Dict, index: int, is_sub: bool = False, is_ultim
return md 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(): def main():
"""메인 실행 함수""" """메인 실행 함수"""
print("="*80) print("="*80)
@ -527,7 +750,7 @@ def main():
elif intermediate_file.exists(): elif intermediate_file.exists():
data_file = intermediate_file data_file = intermediate_file
print(f"\n[ 중간 데이터 사용 ]: {data_file}") print(f"\n[ 중간 데이터 사용 ]: {data_file}")
print("⚠️ 검증되지 않은 데이터입니다. validate_stalker_data.py를 먼저 실행하는 것을 권장합니다.") print("[WARN] 검증되지 않은 데이터입니다. validate_stalker_data.py를 먼저 실행하는 것을 권장합니다.")
else: else:
print(f"[FAIL] 데이터 파일 없음") print(f"[FAIL] 데이터 파일 없음")
print("먼저 extract_stalker_data_v2.py를 실행하세요.") print("먼저 extract_stalker_data_v2.py를 실행하세요.")
@ -540,11 +763,13 @@ def main():
# 마크다운 생성 # 마크다운 생성
md_content = generate_header() md_content = generate_header()
md_content += generate_analysis_prerequisites(data) # 분석 전제조건 추가
md_content += generate_stalker_overview(data) md_content += generate_stalker_overview(data)
md_content += generate_ultimate_overview(data) md_content += generate_ultimate_overview(data)
md_content += generate_dot_overview(data) # DoT 스킬 종합 md_content += generate_dot_overview(data) # DoT 스킬 종합
# 개별 스토커 # 개별 스토커
stalker_count = 0
for stalker_id in config.STALKERS: for stalker_id in config.STALKERS:
if stalker_id not in data: if stalker_id not in data:
print(f"[WARN] {stalker_id}: 데이터 없음, 건너뜀") print(f"[WARN] {stalker_id}: 데이터 없음, 건너뜀")
@ -552,20 +777,26 @@ def main():
print(f" - {stalker_id} 문서 생성 중...") print(f" - {stalker_id} 문서 생성 중...")
md_content += generate_stalker_detail(stalker_id, data[stalker_id]) md_content += generate_stalker_detail(stalker_id, data[stalker_id])
stalker_count += 1
# 특수 시스템 상세 추가
md_content += generate_special_systems(data)
# Footer # Footer
md_content += "---\n\n" md_content += "---\n\n"
md_content += f"**생성 일시**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" md_content += f"**생성 일시**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
md_content += f"**데이터 소스**: {data_file.name}\n" md_content += f"**데이터 소스**: {data_file.name}\n"
md_content += f"**검증 상태**: {'검증 완료' if data_file.name == 'validated_data.json' else '미검증 ⚠️'}\n" md_content += f"**검증 상태**: {'검증 완료' if data_file.name == 'validated_data.json' else '미검증'}\n"
# 파일 저장 # 파일 저장 - 새 파일명 사용
output_file = config.OUTPUT_DIR / "03_스토커별_기본데이터_v2.md" output_file = config.OUTPUT_DIR / "01_분석_기초자료_v2.md"
with open(output_file, 'w', encoding='utf-8') as f: with open(output_file, 'w', encoding='utf-8') as f:
f.write(md_content) f.write(md_content)
print(f"\n[OK] 문서 생성 완료: {output_file}") print(f"\n[OK] 문서 생성 완료: {output_file}")
print(f" - 총 {len(data)}명 스토커 문서 생성") print(f" - 총 {stalker_count}명 스토커 문서 생성")
print(f" - 분석 전제조건 포함")
print(f" - 특수 시스템 상세 포함")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

121
분석도구/v2/output.txt Normal file
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff