# -*- coding: utf-8 -*- """ 마크다운 문서 생성 모듈 """ import re from typing import Dict, List def format_skill_description(desc: str, desc_values: List) -> str: """ 스킬 설명 문자열의 {0}, {1} 등을 descValues로 치환하고 줄바꿈 태그 제거 Args: desc: 원본 설명 문자열 (예: "검을 휘둘러 {0}%만큼 번개 속성 물리 피해를 입힙니다.") desc_values: 치환할 값들의 배열 (예: [130, 0]) Returns: 완성된 설명 문자열 """ if not desc: return '' result = desc # {0}, {1}, {2} 등을 descValues로 치환 for i, value in enumerate(desc_values): placeholder = '{' + str(i) + '}' result = result.replace(placeholder, str(value)) # 줄바꿈 태그 제거 (\r\n, \n,
,
등) result = result.replace('\\r\\n', ' ') result = result.replace('\\n', ' ') result = result.replace('\r\n', ' ') result = result.replace('\n', ' ') result = re.sub(r'', ' ', result) # 연속된 공백을 하나로 result = re.sub(r'\s+', ' ', result) return result.strip() def format_stat_table(stats: Dict) -> str: """기본 스탯 테이블 생성""" stat_rows = [ ('힘 (Str)', stats.get('str', 0)), ('민첩 (Dex)', stats.get('dex', 0)), ('지능 (Int)', stats.get('int', 0)), ('체력 (Con)', stats.get('con', 0)), ('지혜 (Wis)', stats.get('wis', 0)), ('HP', stats.get('hp', 0)), ('MP', stats.get('mp', 0)), ('마나 재생', stats.get('manaRegen', 0)), ('지구력 (Stamina)', stats.get('stamina', 0)), ('크리티컬 확률 (%)', stats.get('criticalPer', 0)) ] lines = ['| 스탯 | 값 |', '|------|-----|'] for name, value in stat_rows: lines.append(f'| {name} | {value} |') return '\n'.join(lines) def format_skill_section(skill_data: Dict, skill_montages: List[Dict]) -> str: """개별 스킬 섹션 생성""" lines = [] # 스킬 기본 정보 skill_id = skill_data.get('skillId', '') skill_name = skill_data.get('name', '') lines.append(f"#### {skill_id} - {skill_name}") lines.append('') # 스킬 설명 (descValues 적용) desc = skill_data.get('desc', '') desc_values = skill_data.get('descValues', []) formatted_desc = format_skill_description(desc, desc_values) if formatted_desc: lines.append(f'**설명**: {formatted_desc}') lines.append('') # 스킬 속성 lines.append('**스킬 속성**') lines.append('| 항목 | 값 |') lines.append('|------|-----|') lines.append(f"| 공격 타입 | {skill_data.get('skillAttackType', '')} |") lines.append(f"| 원소 타입 | {skill_data.get('skillElementType', '')} |") damage_rate = skill_data.get('skillDamageRate', 1.0) lines.append(f"| 피해 배율 | {int(damage_rate * 100)}% |") if skill_data.get('walkSpeedMultiplier', 0) != 0: walk_speed = skill_data.get('walkSpeedMultiplier', 0) lines.append(f"| 이동 속도 배율 | {walk_speed}x |") casting_time = skill_data.get('castingTime', 0) if casting_time > 0: lines.append(f"| 시전 시간 | {casting_time}초 |") mana_cost = skill_data.get('manaCost', 0) if mana_cost > 0: lines.append(f"| 마나 비용 | {mana_cost} |") cool_time = skill_data.get('coolTime', 0) if cool_time > 0: lines.append(f"| 재사용 대기시간 | {cool_time}초 |") # 스택 정보 if skill_data.get('bIsStackable', False): max_stack = skill_data.get('maxStackCount', 0) lines.append(f"| 스택 | 가능 (최대 {max_stack}) |") # 궁극기 여부 if skill_data.get('bIsUltimate', False): lines.append(f"| 궁극기 | O |") # 지속 시간 active_duration = skill_data.get('activeDuration', 0) if active_duration > 0: lines.append(f"| 지속 시간 | {active_duration}초 |") lines.append('') # 어빌리티 클래스 ability_class = skill_data.get('abilityClass', '') if ability_class and ability_class != 'None': class_name = ability_class.split('/')[-1].replace('.', ' → ') lines.append(f'**어빌리티 클래스**: `{class_name}`') lines.append('') active_ability = skill_data.get('activeAbilityClass', '') if active_ability and active_ability != 'None': class_name = active_ability.split('/')[-1].replace('.', ' → ') lines.append(f'**활성 어빌리티**: `{class_name}`') lines.append('') # GameplayEffect ge_set = skill_data.get('gameplayEffectSet', []) if ge_set: lines.append('**Gameplay Effects**') for ge in ge_set: trigger = ge.get('trigger', '') ge_class = ge.get('gEClass', '') ge_name = ge_class.split('/')[-1].replace('.', ' → ') if ge_class else '' lines.append(f'- `{trigger}`: {ge_name}') lines.append('') # 몽타주 정보 if skill_montages: lines.append('---') lines.append('') for i, montage in enumerate(skill_montages, 1): asset_name = montage.get('assetName', '') lines.append(f'**몽타주 {i}: {asset_name}**') lines.append('') seq_len = montage.get('sequenceLength', 0) rate_scale = montage.get('rateScale', 1.0) lines.append(f'- 시퀀스 길이: {seq_len:.2f}초') lines.append(f'- 재생 속도: {rate_scale}x') lines.append('') # AnimNotifies anim_notifies = montage.get('animNotifies', []) if anim_notifies: lines.append('**주요 타이밍**') for notify in anim_notifies: notify_class = notify.get('notifyClass', '') trigger_time = notify.get('triggerTime', 0) duration = notify.get('duration', 0) notify_name = notify_class.replace('_C', '').replace('AnimNotifyState_', '') if duration > 0: end_time = trigger_time + duration lines.append(f'- **{notify_name}**: {trigger_time:.2f}~{end_time:.2f}초 (지속: {duration:.2f}초)') else: lines.append(f'- **{notify_name}**: {trigger_time:.2f}초') # Properties props = notify.get('properties', {}) if props: for key, value in props.items(): if key == 'AttackTag': lines.append(f' - 공격 태그: `{value}`') elif key == 'EventTag': lines.append(f' - 이벤트 태그: `{value}`') elif key == 'AddNormalAttackPer': lines.append(f' - 일반 공격력 증가: {value}%') elif key == 'AddPhysicalAttackPer': lines.append(f' - 물리 공격력 증가: {value}%') lines.append('') lines.append('') lines.append('') return '\n'.join(lines) def format_basic_attack_section(weapon_type: str, montages: List[Dict]) -> str: """기본 공격 섹션 생성""" lines = [] lines.append(f'### 기본 공격: {weapon_type}') lines.append('') for i, montage in enumerate(montages, 1): asset_name = montage.get('assetName', '') lines.append(f'#### 콤보 {i}: {asset_name}') lines.append('') seq_len = montage.get('sequenceLength', 0) rate_scale = montage.get('rateScale', 1.0) lines.append(f'- 시퀀스 길이: {seq_len:.2f}초') lines.append(f'- 재생 속도: {rate_scale}x') lines.append('') # AnimNotifies anim_notifies = montage.get('animNotifies', []) if anim_notifies: lines.append('**주요 타이밍**') for notify in anim_notifies: notify_class = notify.get('notifyClass', '') trigger_time = notify.get('triggerTime', 0) duration = notify.get('duration', 0) notify_name = notify_class.replace('_C', '').replace('AnimNotifyState_', '') if duration > 0: end_time = trigger_time + duration lines.append(f'- **{notify_name}**: {trigger_time:.2f}~{end_time:.2f}초 (지속: {duration:.2f}초)') else: lines.append(f'- **{notify_name}**: {trigger_time:.2f}초') # Properties props = notify.get('properties', {}) if props: for key, value in props.items(): if key == 'AttackTag': lines.append(f' - 공격 태그: `{value}`') elif key == 'AddNormalAttackPer': lines.append(f' - 일반 공격력 증가: {value}%') elif key == 'AddPhysicalAttackPer': lines.append(f' - 물리 공격력 증가: {value}%') lines.append('') lines.append('') return '\n'.join(lines) def generate_stalker_markdown(stalker_name: str, stalker_data: Dict) -> str: """ 스토커 개별 마크다운 문서 생성 Args: stalker_name: 스토커 이름 (소문자) stalker_data: 스토커 전투 데이터 Returns: 마크다운 문서 문자열 """ lines = [] # 타이틀 display_name = stalker_data.get('basic_info', {}).get('name', stalker_name.capitalize()) job_name = stalker_data.get('basic_info', {}).get('jobName', '') lines.append(f'# {display_name} ({stalker_name.capitalize()}) - 전투 데이터') lines.append('') # 기본 정보 lines.append('## 기본 정보') lines.append('') lines.append(f'- **직업**: {job_name}') ultimate_point = stalker_data.get('skills', {}).get('ultimatePoint', 0) lines.append(f'- **궁극기 포인트**: {ultimate_point}') equip_types = stalker_data.get('stats', {}).get('equipableTypes', []) if equip_types: lines.append(f'- **장착 가능 장비**: {", ".join(equip_types)}') lines.append('') # 기본 스탯 lines.append('## 기본 스탯') lines.append('') stats = stalker_data.get('stats', {}) lines.append(format_stat_table(stats)) lines.append('') # 스킬 lines.append('## 스킬') lines.append('') # 기본 스킬 default_skills = stalker_data.get('skills', {}).get('default', []) if default_skills: lines.append('### 기본 스킬') lines.append('') skill_details = stalker_data.get('skill_details', {}) for skill_id in default_skills: skill_data = skill_details.get(skill_id) if skill_data: skill_montages = skill_data.get('montages', []) lines.append(format_skill_section(skill_data, skill_montages)) # 서브 스킬 sub_skill = stalker_data.get('skills', {}).get('sub', '') if sub_skill: lines.append('### 서브 스킬') lines.append('') skill_details = stalker_data.get('skill_details', {}) skill_data = skill_details.get(sub_skill) if skill_data: skill_montages = skill_data.get('montages', []) lines.append(format_skill_section(skill_data, skill_montages)) # 궁극기 ultimate_skill = stalker_data.get('skills', {}).get('ultimate', '') if ultimate_skill: lines.append('### 궁극기') lines.append('') skill_details = stalker_data.get('skill_details', {}) skill_data = skill_details.get(ultimate_skill) if skill_data: skill_montages = skill_data.get('montages', []) lines.append(format_skill_section(skill_data, skill_montages)) # 기본 공격 basic_attacks = stalker_data.get('basic_attacks', {}) if basic_attacks: lines.append('## 기본 공격') lines.append('') for weapon_type, montages in basic_attacks.items(): if montages: lines.append(format_basic_attack_section(weapon_type, montages)) return '\n'.join(lines)