Files
DS-Combat_analy/분석도구/v2/generate_stalker_docs_v2.py
2025-10-28 12:34:12 +09:00

803 lines
30 KiB
Python

#!/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']
duration = attack['actualDuration']
multiplier = attack['attackMultiplier']
mult_display = f"{multiplier:+.1f}" if multiplier != 0 else "0.0"
# 태그 추가
tag = get_montage_tag(montage_name)
note = tag if tag else ""
md += f"| {idx} | {montage_name} | {duration:.2f} | {mult_display} | {note} |\n"
md += "\n"
# 기본 스킬
md += "### 스킬 목록\n\n"
md += "**기본 스킬**:\n\n"
default_skills = stalker_data.get('defaultSkills', [])
for idx, skill in enumerate(default_skills, 1):
if not skill:
continue
md += generate_skill_entry(skill, idx)
# 서브 스킬
sub_skill = stalker_data.get('subSkill')
if sub_skill:
md += "\n**서브 스킬**:\n\n"
md += generate_skill_entry(sub_skill, 0, is_sub=True)
# 궁극기
ultimate_skill = stalker_data.get('ultimateSkill')
if ultimate_skill:
md += "\n**궁극기**:\n\n"
md += generate_skill_entry(ultimate_skill, 0, is_ultimate=True)
# 소환체 (레네만)
summons = stalker_data.get('summons', {})
if summons:
md += "\n### 소환체\n\n"
for summon_name, summon_data in summons.items():
md += generate_summon_entry(summon_name, summon_data)
md += "\n---\n\n"
return md
def generate_summon_entry(summon_name: str, summon_data: Dict) -> str:
"""소환체 엔트리 생성"""
summon_skill_id = summon_data.get('summonSkillId', 'N/A')
summon_skill_name = summon_data.get('summonSkillName', 'N/A')
active_duration = summon_data.get('activeDuration', 0)
skill_damage_rate = summon_data.get('skillDamageRate', 0)
attack_montages = summon_data.get('attackMontages', [])
dot_type = summon_data.get('dotType', '')
# 소환체 타입별 아이콘
icon = ''
if 'ifrit' in summon_name.lower() or '화염' in summon_skill_name:
icon = '🔥'
elif 'shiva' in summon_name.lower() or '냉기' in summon_skill_name or '얼음' in summon_skill_name:
icon = '❄️'
md = f"#### {icon} {summon_name}\n\n"
md += f"- **소환 스킬**: {summon_skill_id} {summon_skill_name}\n"
if active_duration > 0:
md += f"- **소환 유지 시간**: {active_duration}\n"
# 공격 몽타주 정보 및 DPS 계산
if attack_montages:
md += f"- **공격 몽타주**: \n"
# 공격 사이클 계산 (순차적 반복)
total_cycle_time = 0
montage_durations = []
for montage in attack_montages:
montage_name = montage.get('montageName', 'N/A')
duration = montage.get('actualDuration', 0)
md += f" - {montage_name} ({duration:.2f}초)\n"
total_cycle_time += duration
montage_durations.append(duration)
# 공격 사이클 및 DPS 계산
if len(attack_montages) > 0 and total_cycle_time > 0:
# 공격 사이클 표시
if len(attack_montages) == 1:
# 몽타주 1개
md += f"- **공격 사이클**: {montage_durations[0]:.2f}초 (반복)\n"
else:
# 몽타주 2개 이상: 순차 표시 + 총 합계
cycle_str = "".join([f"{d:.2f}" for d in montage_durations])
md += f"- **공격 사이클**: {cycle_str} (총 {total_cycle_time:.2f}초, 반복)\n"
# 예상 공격 횟수 계산
if active_duration > 0:
cycle_count = active_duration / total_cycle_time
attack_count = cycle_count * len(attack_montages)
total_damage = attack_count * skill_damage_rate
md += f"- **예상 공격 횟수**: ~{attack_count:.1f}\n"
md += f"- **총 피해 배율**: ~{total_damage:.2f}배 상당\n"
if dot_type:
dot_config = config.DOT_DAMAGE.get(dot_type, {})
dot_desc = dot_config.get('description', f'{dot_type} DoT')
md += f"- **특수 효과**: {dot_type} DoT ({dot_desc})\n"
md += "\n"
return md
def generate_skill_entry(skill: Dict, index: int, is_sub: bool = False, is_ultimate: bool = False) -> str:
"""개별 스킬 엔트리 생성"""
skill_id = skill.get('skillId', 'N/A')
name = skill.get('name', 'N/A')
skill_type = skill.get('skillAttackType', 'Normal')
element = skill.get('skillElementType', 'None')
damage_rate = skill.get('skillDamageRate', 0)
cooltime = skill.get('coolTime', 0)
mana = skill.get('manaCost', 0)
casting_time = skill.get('castingTime', 0)
# 설명 (descFormatted 사용 - 변수 치환 및 줄바꿈 제거됨)
desc = skill.get('descFormatted', skill.get('simpleDesc', ''))
md = ""
if index > 0:
md += f"{index}. "
md += f"**{skill_id} {name}**\n"
md += f" - **타입**: {skill_type}"
if element and element != 'None':
md += f" / **속성**: {element}"
md += "\n"
if damage_rate > 0:
md += f" - **피해 배율**: {damage_rate}\n"
# 쿨타임, 마나, 시전시간 표시
if cooltime > 0 or mana > 0 or casting_time > 0:
parts = []
if cooltime > 0:
parts.append(f"**쿨타임**: {cooltime}")
if mana > 0:
parts.append(f"**마나**: {mana}")
if casting_time > 0:
parts.append(f"**시전시간**: {casting_time}")
if parts:
md += f" - {' / '.join(parts)}\n"
# 특수 마커
is_dot = skill.get('isDot', False)
is_summon = skill.get('isSummon', False)
is_utility = skill.get('isUtility', False)
# 유틸리티 스킬 표시
if is_utility:
md += f" - 💡 **유틸리티 스킬** (DPS 계산 제외)\n"
if is_dot:
dot_info = config.DOT_SKILLS.get(skill_id, {})
dot_type = dot_info.get('dot_type', 'DoT')
# DoT 피해 상세 정보
if dot_type == 'Poison':
dot_detail = "대상 MaxHP의 20% (5초간)"
elif dot_type == 'Burn':
dot_detail = "대상 MaxHP의 10% (3초간)"
elif dot_type == 'Bleed':
dot_detail = "고정 20 피해 (5초간)"
else:
dot_detail = "지속 피해"
md += f" - ⚠️ **{dot_type} 상태이상 유발**: {dot_detail}\n"
md += f" - 💡 **DoT 피해는 대상 HP에 비례** (구체적 DPS는 다음 챕터 참조)\n"
if is_summon:
summon_info = config.SUMMON_SKILLS.get(skill_id, {})
summon_name = summon_info.get('summon', 'Summon')
duration = skill.get('activeDuration', 0)
md += f" - 🔮 **소환**: {summon_name} (지속 {duration}초)\n"
# 몽타주 정보 표시 (이름 + 시간 + 태그)
montage_data = skill.get('montageData', [])
if montage_data:
if len(montage_data) == 1:
# 몽타주 1개: 한 줄로 표시
montage = montage_data[0]
montage_name = montage.get('assetName', 'N/A')
tag = get_montage_tag(montage_name)
tag_display = f" {tag}" if tag else ""
md += f" - **몽타주**: {montage_name}{tag_display}\n"
else:
# 몽타주 여러 개: 리스트로 표시
md += f" - **몽타주**: \n"
for idx, montage in enumerate(montage_data, 1):
montage_name = montage.get('assetName', 'N/A')
duration = montage.get('actualDuration', 0)
tag = get_montage_tag(montage_name)
tag_display = f" {tag}" if tag else ""
md += f" {idx}. {montage_name} ({duration:.2f}초){tag_display}\n"
# 시퀀스 길이 (새로운 계산 규칙 적용)
sequence_length, is_average, included_montages = calculate_sequence_length(skill_id, montage_data)
if sequence_length > 0 or len(montage_data) > 0:
# 평균 표시 추가
avg_text = " (평균)" if is_average else ""
md += f" - **시퀀스 길이**: {sequence_length:.2f}{avg_text}\n"
# 설명 (전체 표시)
if desc:
md += f" - **설명**: {desc}\n"
md += "\n"
return md
def 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()