340 lines
12 KiB
Python
340 lines
12 KiB
Python
# -*- 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, <br>, <br/> 등)
|
|
result = result.replace('\\r\\n', ' ')
|
|
result = result.replace('\\n', ' ')
|
|
result = result.replace('\r\n', ' ')
|
|
result = result.replace('\n', ' ')
|
|
result = re.sub(r'<br\s*/?>', ' ', 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)
|