#!/usr/bin/env python3 """ 스토커 기본 데이터 문서 생성 스크립트 v2 validated_data.json (또는 intermediate_data.json)에서 03_스토커별_기본데이터_v2.md 생성 """ import json import sys from pathlib import Path from typing import Dict, List from datetime import datetime # config 임포트 sys.path.append(str(Path(__file__).parent)) import config def generate_header() -> str: """문서 헤더 생성""" return f"""# 01. 분석 기초자료 (v2) ## 📌 문서 개요 본 문서는 던전 스토커즈 전투 시스템의 **기초 데이터**를 종합 정리한 자료입니다. ### 구성 1. **분석 전제조건**: 레벨, 기어스코어, 룬 빌드, 장비 스탯 추정 2. **스토커별 기본 데이터**: 10명 스토커의 스탯, 스킬, 평타 정보 3. **특수 시스템 상세**: Parrying, Chain Score, Reload, Charging 등 ## 데이터 소스 - `DT_CharacterStat`: 기본 스탯, 스킬 목록 - `DT_CharacterAbility`: 평타 몽타주 - `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 시전시간, 효과) - `DT_Rune`, `DT_RuneGroup`: 룬 시스템 데이터 - `DT_Equip`, `DT_Float`: 장비 및 기어스코어 상수 - `Blueprint`: 스킬 변수 (ActivationOrderGroup 등) - `AnimMontage`: 타이밍 및 공격 노티파이, 실제 발사 시점 ## 검증 상태 - ✅ 모든 데이터는 최신 JSON에서 추출 - ✅ 교차 검증 완료 - ✅ 출처 명시 (각 데이터 필드별) --- """ def generate_stalker_overview(data: Dict) -> str: """10명 스토커 종합 비교표""" md = "## 10명 스토커 종합 비교표\n\n" md += "| 스토커 | 직업 | STR | DEX | INT | CON | WIS | 궁극기 | 장착 무기 | 평타 |\n" md += "|--------|------|-----|-----|-----|-----|-----|--------|-----------|------|\n" for stalker_id in config.STALKERS: if stalker_id not in data: continue stalker = data[stalker_id] stats = stalker['stats'] st = stats['stats'] # 궁극기 has_ultimate = "⭐" if stats['ultimateSkill'] else "" # 장착 무기 equip_types = ', '.join(stats['equipableTypes']) # 평타 콤보 attack_map = stalker['abilities'].get('attackMontageMap', {}) combo_counts = [] for weapon_type, montage_data in attack_map.items(): count = len(montage_data.get('montageArray', [])) combo_counts.append(f"{count}타") combo_str = ', '.join(combo_counts) if combo_counts else "N/A" md += f"| **{stats['name']}** | {stats['jobName']} | {st['str']} | {st['dex']} | {st['int']} | {st['con']} | {st['wis']} | {has_ultimate} | {equip_types} | {combo_str} |\n" md += "\n**특징**:\n" md += "- **모든 스토커가 궁극기 보유**\n" md += "- 모든 스토커 스탯 합계: 75 포인트 (균형)\n" md += "- HP/MP 동일: 100/50\n" md += "- 마나 회복: 0.2/초 (전원 동일)\n\n" md += "---\n\n" return md def generate_ultimate_overview(data: Dict) -> str: """궁극기 종합 비교""" md = "## 궁극기 종합 비교\n\n" md += "| 스토커 | 궁극기 이름 | 타입 | 피해배율 | 지속/시전 | 주요 효과 |\n" md += "|--------|-------------|------|----------|-----------|----------|\n" for stalker_id in config.STALKERS: if stalker_id not in data: continue stalker = data[stalker_id] ultimate_skill = stalker.get('ultimateSkill') if not ultimate_skill: continue name = ultimate_skill.get('name', 'N/A') skill_type = ultimate_skill.get('skillAttackType', 'Normal') damage_rate = ultimate_skill.get('skillDamageRate', 0) active_duration = ultimate_skill.get('activeDuration', 0) casting_time = ultimate_skill.get('castingTime', 0) # 설명 (descFormatted 사용 - 변수 치환 및 줄바꿈 제거됨) desc = ultimate_skill.get('descFormatted', ultimate_skill.get('simpleDesc', ''))[:100] stalker_name = stalker['stats']['name'] md += f"| **{stalker_name}** | {name} | {skill_type} | {damage_rate} | {active_duration}초 / {casting_time}초 | {desc}... |\n" md += "\n---\n\n" return md def generate_dot_overview(data: Dict) -> str: """DoT 스킬 종합 비교""" md = "## DoT 스킬 종합 비교\n\n" md += "다음 스킬들은 DoT(Damage over Time) 효과가 있으며, **DPS 계산 시 추가 지속 피해를 고려해야 합니다**.\n\n" md += "| 스토커 | 스킬 이름 | DoT 타입 | 기본 피해 | DoT 피해 | 지속시간 |\n" md += "|--------|----------|----------|----------|----------|----------|\n" # config.DOT_SKILLS에서 DoT 스킬 정보 가져오기 for skill_id, dot_info in config.DOT_SKILLS.items(): stalker_id = dot_info['stalker'] if stalker_id not in data: continue stalker = data[stalker_id] stalker_name = stalker['stats']['name'] skills = stalker.get('skills', {}) if skill_id not in skills: continue skill = skills[skill_id] skill_name = skill.get('name', 'N/A') dot_type = dot_info.get('dot_type', 'DoT') damage_rate = skill.get('skillDamageRate', 0) # DoT 피해 설명 if dot_type == 'Poison': dot_damage = "대상 MaxHP의 20%" duration = "5초" elif dot_type == 'Burn': dot_damage = "대상 MaxHP의 10%" duration = "3초" elif dot_type == 'Bleed': dot_damage = "고정 20 피해" duration = "5초" else: dot_damage = "N/A" duration = "N/A" md += f"| **{stalker_name}** | {skill_name} | {dot_type} | {damage_rate} | {dot_damage} | {duration} |\n" md += "\n**주의사항**:\n" md += "- DoT 피해는 대상의 HP에 비례하므로, 적의 체력에 따라 실제 피해량이 달라집니다.\n" md += "- 구체적인 DoT DPS 계산 방법은 다음 챕터에서 다룹니다.\n" md += "- 위 표의 '기본 피해'는 스킬의 skillDamageRate입니다.\n\n" md += "---\n\n" return md def get_montage_tag(montage_name: str) -> str: """ 몽타주 이름에서 태그 추출 Args: montage_name: 몽타주 이름 Returns: 태그 문자열 (예: "[준비]", "[장비]") 또는 빈 문자열 """ montage_tags = config.SEQUENCE_CALCULATION_RULES.get('montage_tags', {}) exclude_keywords = config.SEQUENCE_CALCULATION_RULES.get('exclude_keywords', []) for keyword in exclude_keywords: if keyword.lower() in montage_name.lower(): return montage_tags.get(keyword, '') return '' def calculate_sequence_length(skill_id: str, montage_data: List[Dict]) -> tuple: """ 스킬의 시퀀스 길이 계산 Args: skill_id: 스킬 ID montage_data: 몽타주 데이터 리스트 Returns: (sequence_length, is_average, included_montages) - sequence_length: 계산된 시퀀스 길이 - is_average: 평균 계산 여부 - included_montages: 계산에 포함된 몽타주 리스트 (인덱스) """ if not montage_data: return 0, False, [] rules = config.SEQUENCE_CALCULATION_RULES exclude_keywords = rules.get('exclude_keywords', []) average_skills = rules.get('average_skills', []) exclude_montages = rules.get('exclude_montages', {}) exclude_montage_indices = rules.get('exclude_montage_indices', {}) # 1. 특정 몽타주 제외 리스트 가져오기 skill_exclude_list = exclude_montages.get(skill_id, []) skill_exclude_indices = exclude_montage_indices.get(skill_id, []) # 2. 포함될 몽타주 필터링 included_montages = [] for idx, montage in enumerate(montage_data): montage_name = montage.get('assetName', '') # 인덱스로 제외 체크 if idx in skill_exclude_indices: continue # 특정 몽타주 제외 체크 if montage_name in skill_exclude_list: continue # 키워드 제외 체크 (대소문자 구분 없음) has_exclude_keyword = any( keyword.lower() in montage_name.lower() for keyword in exclude_keywords ) if not has_exclude_keyword: included_montages.append(idx) # 3. 포함된 몽타주가 없으면 0 반환 if not included_montages: return 0, False, [] # 4. 시퀀스 길이 계산 is_average = skill_id in average_skills if is_average: # 평균 계산 total = sum(montage_data[idx].get('actualDuration', 0) for idx in included_montages) sequence_length = total / len(included_montages) if included_montages else 0 else: # 합산 계산 sequence_length = sum(montage_data[idx].get('actualDuration', 0) for idx in included_montages) return sequence_length, is_average, included_montages def generate_stalker_detail(stalker_id: str, stalker_data: Dict) -> str: """개별 스토커 상세 정보""" stats = stalker_data['stats'] st = stats['stats'] info = config.STALKER_INFO.get(stalker_id, {}) # stats['name']은 이미 "English (Korean)" 형식 md = f"## {config.STALKERS.index(stalker_id) + 1}. {stats['name']} - {info.get('role', stats['jobName'])}\n\n" # 기본 정보 md += "### 기본 정보\n" md += f"- **역할**: {info.get('role', 'N/A')}\n" md += f"- **주 스탯**: " # 주 스탯 찾기 (가장 높은 2개) stat_pairs = [(k.upper(), v) for k, v in st.items()] stat_pairs.sort(key=lambda x: x[1], reverse=True) md += f"{stat_pairs[0][0]} {stat_pairs[0][1]}, {stat_pairs[1][0]} {stat_pairs[1][1]}\n" md += f"- **HP**: {stats['hp']} | **MP**: {stats['mp']} | **마나 회복**: {stats['manaRegen']}/초\n" # 크리티컬 스탯 crit_per = stats.get('criticalPer', 5) crit_dmg = stats.get('criticalDamage', 0) md += f"- **크리티컬**: 확률 {crit_per}% | 추가 피해 {crit_dmg}%\n" # 장착 무기 equip_types = ', '.join(stats['equipableTypes']) md += f"- **장착 가능**: {equip_types}\n" # 평타 attack_map = stalker_data['abilities'].get('attackMontageMap', {}) if attack_map: combo_info = [] for weapon_type, montage_data in attack_map.items(): count = len(montage_data.get('montageArray', [])) combo_info.append(f"{weapon_type} {count}타") md += f"- **평타**: {', '.join(combo_info)}\n" md += "\n" # 평타 상세 정보 basic_attacks = stalker_data.get('basicAttacks', {}) if basic_attacks: md += "### 평타 상세 정보\n\n" for weapon_type, attacks in basic_attacks.items(): if attacks: md += f"**{weapon_type}** ({len(attacks)}타 콤보):\n\n" md += "| 타수 | 몽타주 | 실제 시간(초) | 배율(%) | 비고 |\n" md += "|------|--------|---------------|---------|------|\n" for attack in attacks: idx = attack['index'] montage_name = attack['montageName'] # effectiveDuration 사용 (ANS_AttackState 종료 시점 우선) duration = attack.get('effectiveDuration', attack['actualDuration']) multiplier = attack['attackMultiplier'] mult_display = f"{multiplier:+.1f}" if multiplier != 0 else "0.0" # 비고: ANS_AttackState 적용 여부 표시 notes = [] tag = get_montage_tag(montage_name) if tag: notes.append(tag) if attack.get('attackStateEndTime') is not None: notes.append(f"ANS_AttackState: {attack['attackStateEndTime']:.2f}초") note = ", ".join(notes) if notes else "" md += f"| {idx} | {montage_name} | {duration:.2f} | {mult_display} | {note} |\n" md += "\n" # 기본 스킬 md += "### 스킬 목록\n\n" md += "**기본 스킬**:\n\n" default_skills = stalker_data.get('defaultSkills', []) for idx, skill in enumerate(default_skills, 1): if not skill: continue md += generate_skill_entry(skill, idx) # 서브 스킬 sub_skill = stalker_data.get('subSkill') if sub_skill: md += "\n**서브 스킬**:\n\n" md += generate_skill_entry(sub_skill, 0, is_sub=True) # 궁극기 ultimate_skill = stalker_data.get('ultimateSkill') if ultimate_skill: md += "\n**궁극기**:\n\n" md += generate_skill_entry(ultimate_skill, 0, is_ultimate=True) # 소환체 (레네만) summons = stalker_data.get('summons', {}) if summons: md += "\n### 소환체\n\n" for summon_name, summon_data in summons.items(): md += generate_summon_entry(summon_name, summon_data) md += "\n---\n\n" return md def generate_summon_entry(summon_name: str, summon_data: Dict) -> str: """소환체 엔트리 생성""" summon_skill_id = summon_data.get('summonSkillId', 'N/A') summon_skill_name = summon_data.get('summonSkillName', 'N/A') active_duration = summon_data.get('activeDuration', 0) skill_damage_rate = summon_data.get('skillDamageRate', 0) attack_montages = summon_data.get('attackMontages', []) dot_type = summon_data.get('dotType', '') # 소환체 타입별 아이콘 icon = '' if 'ifrit' in summon_name.lower() or '화염' in summon_skill_name: icon = '🔥' elif 'shiva' in summon_name.lower() or '냉기' in summon_skill_name or '얼음' in summon_skill_name: icon = '❄️' md = f"#### {icon} {summon_name}\n\n" md += f"- **소환 스킬**: {summon_skill_id} {summon_skill_name}\n" if active_duration > 0: md += f"- **소환 유지 시간**: {active_duration}초\n" # 공격 몽타주 정보 및 DPS 계산 if attack_montages: md += f"- **공격 몽타주**: \n" # 공격 사이클 계산 (순차적 반복) total_cycle_time = 0 montage_durations = [] for montage in attack_montages: montage_name = montage.get('montageName', 'N/A') duration = montage.get('actualDuration', 0) md += f" - {montage_name} ({duration:.2f}초)\n" total_cycle_time += duration montage_durations.append(duration) # 공격 사이클 및 DPS 계산 if len(attack_montages) > 0 and total_cycle_time > 0: # 공격 사이클 표시 if len(attack_montages) == 1: # 몽타주 1개 md += f"- **공격 사이클**: {montage_durations[0]:.2f}초 (반복)\n" else: # 몽타주 2개 이상: 순차 표시 + 총 합계 cycle_str = " → ".join([f"{d:.2f}초" for d in montage_durations]) md += f"- **공격 사이클**: {cycle_str} (총 {total_cycle_time:.2f}초, 반복)\n" # 예상 공격 횟수 계산 if active_duration > 0: cycle_count = active_duration / total_cycle_time attack_count = cycle_count * len(attack_montages) total_damage = attack_count * skill_damage_rate md += f"- **예상 공격 횟수**: ~{attack_count:.1f}회\n" md += f"- **총 피해 배율**: ~{total_damage:.2f}배 상당\n" if dot_type: dot_config = config.DOT_DAMAGE.get(dot_type, {}) dot_desc = dot_config.get('description', f'{dot_type} DoT') md += f"- **특수 효과**: {dot_type} DoT ({dot_desc})\n" md += "\n" return md def generate_skill_entry(skill: Dict, index: int, is_sub: bool = False, is_ultimate: bool = False) -> str: """개별 스킬 엔트리 생성""" skill_id = skill.get('skillId', 'N/A') name = skill.get('name', 'N/A') skill_type = skill.get('skillAttackType', 'Normal') element = skill.get('skillElementType', 'None') damage_rate = skill.get('skillDamageRate', 0) cooltime = skill.get('coolTime', 0) mana = skill.get('manaCost', 0) casting_time = skill.get('castingTime', 0) # 설명 (descFormatted 사용 - 변수 치환 및 줄바꿈 제거됨) desc = skill.get('descFormatted', skill.get('simpleDesc', '')) md = "" if index > 0: md += f"{index}. " md += f"**{skill_id} {name}**\n" md += f" - **타입**: {skill_type}" if element and element != 'None': md += f" / **속성**: {element}" md += "\n" if damage_rate > 0: md += f" - **피해 배율**: {damage_rate}\n" # 쿨타임, 마나, 시전시간 표시 if cooltime > 0 or mana > 0 or casting_time > 0: parts = [] if cooltime > 0: parts.append(f"**쿨타임**: {cooltime}초") if mana > 0: parts.append(f"**마나**: {mana}") if casting_time > 0: parts.append(f"**시전시간**: {casting_time}초") if parts: md += f" - {' / '.join(parts)}\n" # 특수 마커 is_dot = skill.get('isDot', False) is_summon = skill.get('isSummon', False) is_utility = skill.get('isUtility', False) # 유틸리티 스킬 표시 if is_utility: md += f" - 💡 **유틸리티 스킬** (DPS 계산 제외)\n" if is_dot: dot_info = config.DOT_SKILLS.get(skill_id, {}) dot_type = dot_info.get('dot_type', 'DoT') # DoT 피해 상세 정보 if dot_type == 'Poison': dot_detail = "대상 MaxHP의 20% (5초간)" elif dot_type == 'Burn': dot_detail = "대상 MaxHP의 10% (3초간)" elif dot_type == 'Bleed': dot_detail = "고정 20 피해 (5초간)" else: dot_detail = "지속 피해" md += f" - ⚠️ **{dot_type} 상태이상 유발**: {dot_detail}\n" md += f" - 💡 **DoT 피해는 대상 HP에 비례** (구체적 DPS는 다음 챕터 참조)\n" if is_summon: summon_info = config.SUMMON_SKILLS.get(skill_id, {}) summon_name = summon_info.get('summon', 'Summon') duration = skill.get('activeDuration', 0) md += f" - 🔮 **소환**: {summon_name} (지속 {duration}초)\n" # 몽타주 정보 표시 (이름 + 시간 + 태그) montage_data = skill.get('montageData', []) if montage_data: if len(montage_data) == 1: # 몽타주 1개: 한 줄로 표시 montage = montage_data[0] montage_name = montage.get('assetName', 'N/A') tag = get_montage_tag(montage_name) tag_display = f" {tag}" if tag else "" md += f" - **몽타주**: {montage_name}{tag_display}\n" else: # 몽타주 여러 개: 리스트로 표시 md += f" - **몽타주**: \n" for idx, montage in enumerate(montage_data, 1): montage_name = montage.get('assetName', 'N/A') duration = montage.get('actualDuration', 0) tag = get_montage_tag(montage_name) tag_display = f" {tag}" if tag else "" md += f" {idx}. {montage_name} ({duration:.2f}초){tag_display}\n" # 시퀀스 길이 (새로운 계산 규칙 적용) sequence_length, is_average, included_montages = calculate_sequence_length(skill_id, montage_data) if sequence_length > 0 or len(montage_data) > 0: # 평균 표시 추가 avg_text = " (평균)" if is_average else "" md += f" - **시퀀스 길이**: {sequence_length:.2f}초{avg_text}\n" # 설명 (전체 표시) if desc: md += f" - **설명**: {desc}\n" md += "\n" return md def generate_analysis_prerequisites(data: Dict) -> str: """분석 전제조건 섹션 생성""" md = "## 📋 분석 전제조건\n\n" # 공통 설정 md += "### 기본 설정\n" md += f"- **레벨**: {config.ANALYSIS_BASELINE['level']}\n" md += f"- **기어 스코어**: {config.ANALYSIS_BASELINE['gear_score']}\n" md += f"- **플레이 스타일**: {config.ANALYSIS_BASELINE['play_style']}\n\n" # 장비 스탯 추정 (metadata에서 추출) md += "### 장비 스탯 추정 (기어스코어 400 기준)\n\n" metadata = data.get('_metadata', {}) gear_constants = metadata.get('gearScoreConstants', {}) md += "**무기** (레벨 20, Rare 등급 기준):\n" md += "- PhysicalDamage: +65\n" md += "- MagicalDamage: +65\n\n" md += "**방어구 3부위** (갑옷, 다리, 액세서리):\n" md += "- 총 PhysicalDamage: +15\n" md += "- 총 MagicalDamage: +15\n" md += "- HP: +120\n" md += "- Defense: +80\n\n" md += "**총 장비 보너스**:\n" md += "- PhysicalDamage: +80\n" md += "- MagicalDamage: +80\n" md += "- HP: +120\n" md += "- Defense: +80\n\n" # 룬 빌드 설정 (metadata에서 룬 데이터 활용) md += "### 역할별 최적 룬 빌드\n\n" runes = metadata.get('runes', {}) rune_groups = metadata.get('runeGroups', {}) # 물리 딜러 빌드 예시 (룬 데이터에서 추출) md += "#### 물리 딜러 (Hilda, Baran, Rio, Sinobu, Cazimord)\n\n" md += "**Main: 스킬 그룹 (20xxx)**\n" md += "- 20101 저주 (조건부 지연 피해)\n" md += "- 20201 파괴 (+10% 스킬 피해)\n" md += "- 20301 명상 (+70% 마나 회복)\n\n" md += "**Sub: 전투 그룹 (10xxx)**\n" md += "- 10201 분노 (+10% 물리 피해)\n" md += "- 10103 공략 (+20% 머리 공격 피해)\n\n" md += "#### 마법 딜러 (Nave, Rene)\n\n" md += "**Main: 스킬 그룹 (20xxx)**\n" md += "- 20103 활기 (마나 높을 때 스킬 피해 증가)\n" md += "- 20202 왜곡 (-25% 쿨타임)\n" md += "- 20301 명상 (+70% 마나 회복)\n\n" md += "**Sub: 전투 그룹 (10xxx)**\n" md += "- 10301 폭풍 (+10% 마법 피해)\n" md += "- 10103 공략 (+20% 머리 공격 피해)\n\n" md += "#### 원거리 딜러 (Urud, Lian)\n\n" md += "**Main: 스킬 그룹 (20xxx)**\n" md += "- 20101 저주 (지연 피해)\n" md += "- 20201 파괴 (+10% 스킬 피해)\n" md += "- 20301 명상 (+70% 마나 회복)\n\n" md += "**Sub: 전투 그룹 (10xxx)**\n" md += "- 10201 분노 (+10% 물리 피해)\n" md += "- 10103 공략 (+20% 머리 공격 피해)\n\n" md += "#### 서포터 (Clad)\n\n" md += "**Main: 전투 그룹 (10xxx)**\n" md += "- 10101 충전 (+30% 궁극기 회복)\n" md += "- 10202 방패 (+7% 물리 저항)\n" md += "- 10302 수호 (+7% 마법 저항)\n\n" md += "**Sub: 보조 그룹 (40xxx)**\n" md += "- 40201 면역 (물약 사용 시 +20% 저항 20초)\n" md += "- 40301 효율 (+50% 물약 효과)\n\n" # 특수 시스템 활용률 md += "### 특수 시스템 활용률\n\n" md += "**전제**: 최적 플레이 = 100% 활용\n\n" md += "#### Cazimord - Parrying (흘리기)\n" md += "- **판정 윈도우**: 0.2초\n" md += "- **성공 시 효과**:\n" md += " - 적 피해 무효화\n" md += " - 자동 반격 (높은 피해)\n" md += " - **스킬 쿨타임 감소**:\n" md += " - 섬광(SK170201): -3.8초\n" md += " - 날개베기(SK170202): -3.8초\n" md += " - 작열(SK170203): -6.8초\n" md += "- **활용률 시나리오**: 0% (미사용) vs 100% (완벽 성공)\n\n" md += "#### Rio - Chain Score\n" md += "- **최대 스택**: 3\n" md += "- **효과**: 각 스킬별로 다른 위력 증가\n" md += "- **충전**: Dropping Attack 성공 시\n" md += "- **활용률**: 100% (항상 3스택 유지)\n\n" md += "#### Urud & Lian - Reload\n" md += "- **탄약**: 6발\n" md += "- **재장전 시간**: 2.0초\n" md += "- **활용률**: 100% (탄약 관리 최적화)\n\n" md += "#### Lian - Charging Bow\n" md += "- **만충전 데미지**: 1.5배\n" md += "- **충전 시간**: 레벨당 0.5초 (최대 1.5초)\n" md += "- **활용률**: 100% (항상 만충전 후 발사)\n\n" md += "#### Rene - Spirit 소환\n" md += "- **소환수**: Ifrit, Shiva\n" md += "- **활용률**: 100% (소환수 항상 활용)\n\n" md += "#### Sinobu - Shuriken 충전\n" md += "- **최대 충전**: 3개\n" md += "- **충전 속도**: 1초/개\n" md += "- **활용률**: 100% (충전 관리 최적화)\n\n" md += "---\n\n" return md def generate_special_systems(data: Dict) -> str: """특수 시스템 상세 분석 섹션 생성""" md = "## 🔧 특수 시스템 상세\n\n" md += "### Cazimord - Parrying (흘리기)\n\n" md += "#### 메커니즘\n" md += "- **판정 윈도우**: 0.2초\n" md += "- **패링 성공 시**:\n" md += " - 적 공격 무효화\n" md += " - 자동 반격 (높은 피해)\n" md += " - 스킬 쿨타임 감소\n\n" md += "#### 쿨타임 감소 효과\n" md += "| 스킬 | 기본 쿨타임 | 패링 성공 시 감소 | 패링 100% 시 유효 쿨타임 |\n" md += "|------|-------------|-------------------|------------------------|\n" # Cazimord 스킬 데이터에서 쿨타임 정보 추출 if 'cazimord' in data: cazimord = data['cazimord'] skills = cazimord.get('skills', {}) parrying_skills = { 'SK170201': ('섬광', -3.8), 'SK170202': ('날개베기', -3.8), 'SK170203': ('작열', -6.8) } for skill_id, (skill_name, reduction) in parrying_skills.items(): if skill_id in skills: skill = skills[skill_id] base_cooltime = skill.get('coolTime', 0) effective_cooltime = max(0, base_cooltime + reduction) md += f"| {skill_name} | {base_cooltime:.1f}초 | {reduction}초 | {effective_cooltime:.1f}초 |\n" md += "\n#### DPS 영향\n" md += "- **패링 0%**: 기본 쿨타임 적용\n" md += "- **패링 100%**: 쿨타임 감소로 스킬 회전율 증가 → DPS 상승\n\n" md += "### Rio - Chain Score\n\n" md += "#### 메커니즘\n" md += "- **스택 시스템**: 최대 3스택\n" md += "- **스택 획득**: Dropping Attack 스킬 성공 시 +1\n" md += "- **효과**: 스킬별로 스택 소모 및 추가 효과 발동\n\n" md += "#### 스택별 효과\n" md += "- 각 스킬이 Chain Score 스택을 소모하여 강화\n" md += "- 스킬마다 다른 위력 증가 배율 적용\n\n" md += "### Urud & Lian - Reload 시스템\n\n" md += "#### 메커니즘\n" md += "- **최대 탄약**: 6발\n" md += "- **재장전 시간**: 2.0초\n" md += "- **재장전 중**: 다른 행동 불가 (DPS 손실)\n\n" md += "#### DPS 영향\n" md += "- 6발 소진 후 2초 공백 발생\n" md += "- 최적 플레이: 탄약 관리로 전투 공백 최소화\n\n" md += "### Lian - Charging Bow\n\n" md += "#### 메커니즘\n" md += "- **충전 단계**: 3단계 (0.5초씩)\n" md += "- **만충전 배율**: 1.5배\n" md += "- **충전 중**: 이동 속도 감소\n\n" md += "#### DPS 영향\n" md += "- 만충전 시 피해량 증가\n" md += "- 충전 시간 vs 피해량 트레이드오프\n\n" md += "### Rene - 소환수 시스템\n\n" md += "#### Ifrit (화염 정령)\n" if 'rene' in data: rene = data['rene'] summons = rene.get('summons', {}) if 'Ifrit' in summons: ifrit = summons['Ifrit'] md += f"- **지속 시간**: {ifrit.get('activeDuration', 0)}초\n" md += f"- **공격 타입**: 근접 화염 공격\n" md += f"- **독립 DPS**: 계산 필요 (소환수 몽타주 기반)\n\n" md += "#### Shiva (냉기 정령)\n" if 'rene' in data and 'Shiva' in rene.get('summons', {}): shiva = summons.get('Shiva', {}) md += f"- **지속 시간**: {shiva.get('activeDuration', 0)}초\n" md += f"- **공격 타입**: 원거리 냉기 공격\n" md += f"- **독립 DPS**: 계산 필요 (소환수 몽타주 기반)\n\n" md += "### Sinobu - Shuriken 충전\n\n" md += "#### 메커니즘\n" md += "- **최대 충전**: 3개\n" md += "- **충전 속도**: 1초/개 (자동)\n" md += "- **소모**: 특정 스킬 사용 시 1개씩 소모\n\n" md += "#### DPS 영향\n" md += "- 충전 관리로 스킬 사용 빈도 조절\n" md += "- 최적 플레이: 충전 타이밍 고려한 스킬 로테이션\n\n" md += "---\n\n" return md def main(): """메인 실행 함수""" print("="*80) print("스토커 기본 데이터 문서 생성 v2") print("="*80) # 검증된 데이터 로드 (없으면 intermediate 사용) validated_file = config.OUTPUT_DIR / "validated_data.json" intermediate_file = config.OUTPUT_DIR / "intermediate_data.json" if validated_file.exists(): data_file = validated_file print(f"\n[ 검증된 데이터 사용 ]: {data_file}") elif intermediate_file.exists(): data_file = intermediate_file print(f"\n[ 중간 데이터 사용 ]: {data_file}") print("[WARN] 검증되지 않은 데이터입니다. validate_stalker_data.py를 먼저 실행하는 것을 권장합니다.") else: print(f"[FAIL] 데이터 파일 없음") print("먼저 extract_stalker_data_v2.py를 실행하세요.") return with open(data_file, 'r', encoding='utf-8') as f: data = json.load(f) print("\n[ 문서 생성 시작 ]") # 마크다운 생성 md_content = generate_header() md_content += generate_analysis_prerequisites(data) # 분석 전제조건 추가 md_content += generate_stalker_overview(data) md_content += generate_ultimate_overview(data) md_content += generate_dot_overview(data) # DoT 스킬 종합 # 개별 스토커 stalker_count = 0 for stalker_id in config.STALKERS: if stalker_id not in data: print(f"[WARN] {stalker_id}: 데이터 없음, 건너뜀") continue print(f" - {stalker_id} 문서 생성 중...") md_content += generate_stalker_detail(stalker_id, data[stalker_id]) stalker_count += 1 # 특수 시스템 상세 추가 md_content += generate_special_systems(data) # Footer md_content += "---\n\n" md_content += f"**생성 일시**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" md_content += f"**데이터 소스**: {data_file.name}\n" md_content += f"**검증 상태**: {'검증 완료' if data_file.name == 'validated_data.json' else '미검증'}\n" # 파일 저장 - 새 파일명 사용 output_file = config.OUTPUT_DIR / "01_분석_기초자료_v2.md" with open(output_file, 'w', encoding='utf-8') as f: f.write(md_content) print(f"\n[OK] 문서 생성 완료: {output_file}") print(f" - 총 {stalker_count}명 스토커 문서 생성") print(f" - 분석 전제조건 포함") print(f" - 특수 시스템 상세 포함") if __name__ == "__main__": main()