20251117_104033 기준 데이터
This commit is contained in:
BIN
데이터수집/수집스크립트/__pycache__/custom_property_parser.cpython-313.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/custom_property_parser.cpython-313.pyc
Normal file
Binary file not shown.
BIN
데이터수집/수집스크립트/__pycache__/custom_property_parser.cpython-314.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/custom_property_parser.cpython-314.pyc
Normal file
Binary file not shown.
BIN
데이터수집/수집스크립트/__pycache__/data_extractors.cpython-313.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/data_extractors.cpython-313.pyc
Normal file
Binary file not shown.
BIN
데이터수집/수집스크립트/__pycache__/data_extractors.cpython-314.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/data_extractors.cpython-314.pyc
Normal file
Binary file not shown.
BIN
데이터수집/수집스크립트/__pycache__/markdown_formatter.cpython-313.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/markdown_formatter.cpython-313.pyc
Normal file
Binary file not shown.
BIN
데이터수집/수집스크립트/__pycache__/markdown_formatter.cpython-314.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/markdown_formatter.cpython-314.pyc
Normal file
Binary file not shown.
BIN
데이터수집/수집스크립트/__pycache__/validators.cpython-313.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/validators.cpython-313.pyc
Normal file
Binary file not shown.
BIN
데이터수집/수집스크립트/__pycache__/validators.cpython-314.pyc
Normal file
BIN
데이터수집/수집스크립트/__pycache__/validators.cpython-314.pyc
Normal file
Binary file not shown.
247
데이터수집/수집스크립트/collect_combat_data.py
Normal file
247
데이터수집/수집스크립트/collect_combat_data.py
Normal file
@ -0,0 +1,247 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DS 전투 데이터 수집 메인 스크립트
|
||||
|
||||
실행 방법:
|
||||
python collect_combat_data.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
from data_extractors import (
|
||||
TARGET_STALKERS,
|
||||
extract_character_stat,
|
||||
extract_attack_montages,
|
||||
extract_skill_data,
|
||||
extract_montage_info
|
||||
)
|
||||
from markdown_formatter import generate_stalker_markdown
|
||||
from validators import validate_collection_result, log_collection_summary
|
||||
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 경로 설정
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SOURCE_DATA_DIR = os.path.join(os.path.dirname(BASE_DIR), '원본데이터')
|
||||
OUTPUT_DIR = os.path.join(BASE_DIR, '수집결과')
|
||||
MARKDOWN_DIR = os.path.join(OUTPUT_DIR, 'markdown')
|
||||
|
||||
DATATABLE_JSON = os.path.join(SOURCE_DATA_DIR, 'DataTable.json')
|
||||
ANIMMONTAGE_JSON = os.path.join(SOURCE_DATA_DIR, 'AnimMontage.json')
|
||||
|
||||
OUTPUT_JSON = os.path.join(OUTPUT_DIR, 'all_stalkers_combat_data.json')
|
||||
LOG_FILE = os.path.join(OUTPUT_DIR, 'collection_log.txt')
|
||||
|
||||
|
||||
def load_json_file(file_path: str) -> dict:
|
||||
"""JSON 파일 로드"""
|
||||
logger.info(f"JSON 파일 로드 중: {file_path}")
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
logger.info(f" - 로드 완료")
|
||||
return data
|
||||
|
||||
|
||||
def collect_stalker_data(
|
||||
stalker_name: str,
|
||||
datatable_assets: List[Dict],
|
||||
animmontage_assets: List[Dict]
|
||||
) -> Dict:
|
||||
"""
|
||||
단일 스토커의 전투 데이터 수집
|
||||
|
||||
Args:
|
||||
stalker_name: 스토커 이름 (소문자)
|
||||
datatable_assets: DataTable.json의 Assets 배열
|
||||
animmontage_assets: AnimMontage.json의 Assets 배열
|
||||
|
||||
Returns:
|
||||
스토커 전투 데이터
|
||||
"""
|
||||
logger.info(f" [{stalker_name}] 데이터 수집 시작")
|
||||
|
||||
# 1. 기본 스탯 추출
|
||||
stats = extract_character_stat(datatable_assets, stalker_name)
|
||||
if not stats:
|
||||
logger.error(f" [{stalker_name}] 스탯 정보 추출 실패")
|
||||
return {}
|
||||
|
||||
# 2. 기본 정보 분리
|
||||
basic_info = {
|
||||
'name': stats.pop('name', ''),
|
||||
'jobName': stats.pop('jobName', '')
|
||||
}
|
||||
|
||||
# 3. 스킬 정보 분리
|
||||
skills = {
|
||||
'default': stats.pop('defaultSkills', []),
|
||||
'sub': stats.pop('subSkill', ''),
|
||||
'ultimate': stats.pop('ultimateSkill', ''),
|
||||
'ultimatePoint': stats.pop('ultimatePoint', 0)
|
||||
}
|
||||
|
||||
# 4. 스킬 상세 정보 및 몽타주 수집
|
||||
skill_details = {}
|
||||
all_skill_ids = skills['default'] + [skills['sub'], skills['ultimate']]
|
||||
|
||||
for skill_id in all_skill_ids:
|
||||
if not skill_id:
|
||||
continue
|
||||
|
||||
skill_data = extract_skill_data(datatable_assets, skill_id)
|
||||
if not skill_data:
|
||||
logger.warning(f" [{stalker_name}] 스킬 {skill_id} 데이터 추출 실패")
|
||||
continue
|
||||
|
||||
# 스킬 몽타주 정보 수집
|
||||
montage_paths = skill_data.get('useMontages', [])
|
||||
montages = []
|
||||
for montage_path in montage_paths:
|
||||
montage_info = extract_montage_info(animmontage_assets, montage_path)
|
||||
if montage_info:
|
||||
montages.append(montage_info)
|
||||
|
||||
skill_data['montages'] = montages
|
||||
skill_details[skill_id] = skill_data
|
||||
|
||||
# 5. 기본 공격 몽타주 수집
|
||||
attack_montages_map = extract_attack_montages(datatable_assets, stalker_name)
|
||||
basic_attacks = {}
|
||||
|
||||
for weapon_type, montage_paths in attack_montages_map.items():
|
||||
montages = []
|
||||
for montage_path in montage_paths:
|
||||
montage_info = extract_montage_info(animmontage_assets, montage_path)
|
||||
if montage_info:
|
||||
montages.append(montage_info)
|
||||
|
||||
if montages:
|
||||
basic_attacks[weapon_type] = montages
|
||||
|
||||
# 6. 최종 데이터 구성
|
||||
stalker_data = {
|
||||
'basic_info': basic_info,
|
||||
'stats': stats,
|
||||
'skills': skills,
|
||||
'skill_details': skill_details,
|
||||
'basic_attacks': basic_attacks
|
||||
}
|
||||
|
||||
logger.info(f" [{stalker_name}] 데이터 수집 완료")
|
||||
logger.info(f" - 스킬 {len(skill_details)}개")
|
||||
logger.info(f" - 무기 타입 {len(basic_attacks)}개")
|
||||
|
||||
return stalker_data
|
||||
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
logger.info("=" * 80)
|
||||
logger.info("DS 전투 데이터 수집 시작")
|
||||
logger.info("=" * 80)
|
||||
|
||||
# 1. 원본 데이터 로드
|
||||
logger.info("1. 원본 데이터 로드 중...")
|
||||
datatable_data = load_json_file(DATATABLE_JSON)
|
||||
animmontage_data = load_json_file(ANIMMONTAGE_JSON)
|
||||
|
||||
datatable_assets = datatable_data.get('Assets', [])
|
||||
animmontage_assets = animmontage_data.get('Assets', [])
|
||||
|
||||
logger.info(f" - DataTable 에셋 수: {len(datatable_assets)}")
|
||||
logger.info(f" - AnimMontage 에셋 수: {len(animmontage_assets)}")
|
||||
logger.info("")
|
||||
|
||||
# 2. 스토커별 데이터 수집
|
||||
logger.info(f"2. 스토커 데이터 수집 중 (총 {len(TARGET_STALKERS)}명)...")
|
||||
stalkers_data = {}
|
||||
|
||||
for stalker_name in TARGET_STALKERS:
|
||||
stalker_data = collect_stalker_data(
|
||||
stalker_name,
|
||||
datatable_assets,
|
||||
animmontage_assets
|
||||
)
|
||||
if stalker_data:
|
||||
stalkers_data[stalker_name] = stalker_data
|
||||
|
||||
logger.info("")
|
||||
|
||||
# 3. 통합 JSON 생성
|
||||
logger.info("3. 통합 JSON 생성 중...")
|
||||
collection_data = {
|
||||
'collection_metadata': {
|
||||
'collected_at': datetime.now().isoformat(),
|
||||
'source_files': [
|
||||
'DataTable.json',
|
||||
'AnimMontage.json'
|
||||
],
|
||||
'total_stalkers': len(stalkers_data)
|
||||
},
|
||||
'stalkers': stalkers_data
|
||||
}
|
||||
|
||||
with open(OUTPUT_JSON, 'w', encoding='utf-8') as f:
|
||||
json.dump(collection_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f" - 통합 JSON 생성 완료: {OUTPUT_JSON}")
|
||||
logger.info("")
|
||||
|
||||
# 4. 스토커별 마크다운 생성
|
||||
logger.info(f"4. 스토커별 마크다운 생성 중 ({len(stalkers_data)}명)...")
|
||||
for stalker_name, stalker_data in stalkers_data.items():
|
||||
markdown_content = generate_stalker_markdown(stalker_name, stalker_data)
|
||||
markdown_path = os.path.join(MARKDOWN_DIR, f'{stalker_name}.md')
|
||||
|
||||
with open(markdown_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
logger.info(f" - {stalker_name}.md 생성 완료")
|
||||
|
||||
logger.info("")
|
||||
|
||||
# 5. 데이터 검증
|
||||
logger.info("5. 데이터 검증 중...")
|
||||
is_valid = validate_collection_result(collection_data)
|
||||
|
||||
if is_valid:
|
||||
logger.info(" ✓ 모든 데이터 검증 통과")
|
||||
else:
|
||||
logger.warning(" ⚠ 일부 데이터 검증 실패 (상세 내용은 로그 참조)")
|
||||
|
||||
logger.info("")
|
||||
|
||||
# 6. 수집 요약 로그 생성
|
||||
logger.info("6. 수집 요약 로그 생성 중...")
|
||||
log_collection_summary(collection_data, LOG_FILE)
|
||||
logger.info("")
|
||||
|
||||
# 7. 완료
|
||||
logger.info("=" * 80)
|
||||
logger.info("DS 전투 데이터 수집 완료!")
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"결과 파일:")
|
||||
logger.info(f" - 통합 JSON: {OUTPUT_JSON}")
|
||||
logger.info(f" - 마크다운: {MARKDOWN_DIR}")
|
||||
logger.info(f" - 수집 로그: {LOG_FILE}")
|
||||
logger.info("")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
logger.exception("수집 중 예외 발생:")
|
||||
raise
|
||||
160
데이터수집/수집스크립트/custom_property_parser.py
Normal file
160
데이터수집/수집스크립트/custom_property_parser.py
Normal file
@ -0,0 +1,160 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CustomProperties 문자열 파싱 유틸리티
|
||||
|
||||
AnimMontage의 AnimNotifies에 있는 CustomProperties는 모두 문자열로 저장되어 있어서
|
||||
타입 변환 및 파싱이 필요합니다.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def parse_float(value: str) -> Optional[float]:
|
||||
"""문자열을 float로 변환 (실패 시 None)"""
|
||||
try:
|
||||
return float(value.strip())
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_tag_name(value: str) -> Optional[str]:
|
||||
"""
|
||||
(TagName="Event.Attack.Normal") 형태에서 태그 이름 추출
|
||||
|
||||
Example:
|
||||
'(TagName="Event.Attack.Normal")' -> 'Event.Attack.Normal'
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
match = re.search(r'TagName="([^"]+)"', value)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def parse_vector(value: str) -> Optional[dict]:
|
||||
"""
|
||||
(X=0.0,Y=1.0,Z=0.0) 형태를 dict로 변환
|
||||
|
||||
Example:
|
||||
'(X=0.0,Y=1.0,Z=0.0)' -> {'X': 0.0, 'Y': 1.0, 'Z': 0.0}
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
x_match = re.search(r'X=([-\d.]+)', value)
|
||||
y_match = re.search(r'Y=([-\d.]+)', value)
|
||||
z_match = re.search(r'Z=([-\d.]+)', value)
|
||||
|
||||
if x_match and y_match and z_match:
|
||||
return {
|
||||
'X': float(x_match.group(1)),
|
||||
'Y': float(y_match.group(1)),
|
||||
'Z': float(z_match.group(1))
|
||||
}
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_array(value: str) -> Optional[list]:
|
||||
"""
|
||||
(0.1,0.2,0.3) 형태를 리스트로 변환
|
||||
|
||||
Example:
|
||||
'(0.1,0.2,0.3)' -> [0.1, 0.2, 0.3]
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 괄호 제거
|
||||
cleaned = value.strip().strip('()')
|
||||
if not cleaned:
|
||||
return []
|
||||
|
||||
# 쉼표로 분리
|
||||
parts = cleaned.split(',')
|
||||
return [float(p.strip()) for p in parts if p.strip()]
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def parse_custom_property(value: str, field_name: str) -> Any:
|
||||
"""
|
||||
CustomProperties 필드의 값을 적절한 타입으로 변환
|
||||
|
||||
Args:
|
||||
value: 원본 문자열 값
|
||||
field_name: 필드 이름 (타입 추론에 사용)
|
||||
|
||||
Returns:
|
||||
파싱된 값 (float, str, dict, list 등)
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
value = value.strip()
|
||||
|
||||
# 빈 문자열
|
||||
if not value or value == '':
|
||||
return None
|
||||
|
||||
# TagName 파싱
|
||||
if 'TagName=' in value:
|
||||
tag = parse_tag_name(value)
|
||||
if tag and tag != 'None':
|
||||
return tag
|
||||
return None
|
||||
|
||||
# Vector 파싱
|
||||
if value.startswith('(') and ('X=' in value or 'Y=' in value or 'Z=' in value):
|
||||
vec = parse_vector(value)
|
||||
if vec:
|
||||
return vec
|
||||
|
||||
# Array 파싱 (TimeArray, LocationArray 등)
|
||||
if 'Array' in field_name and value.startswith('('):
|
||||
arr = parse_array(value)
|
||||
if arr is not None:
|
||||
return arr
|
||||
|
||||
# Boolean 파싱
|
||||
if value.lower() in ['true', 'false']:
|
||||
return value.lower() == 'true'
|
||||
|
||||
# Float 파싱 시도
|
||||
float_val = parse_float(value)
|
||||
if float_val is not None:
|
||||
return float_val
|
||||
|
||||
# 기본: 문자열 그대로 반환
|
||||
return value
|
||||
|
||||
|
||||
def extract_notify_properties(notify: dict, target_fields: list) -> dict:
|
||||
"""
|
||||
AnimNotify에서 CustomProperties의 특정 필드만 추출
|
||||
|
||||
Args:
|
||||
notify: AnimNotify 딕셔너리
|
||||
target_fields: 추출할 필드 이름 리스트
|
||||
|
||||
Returns:
|
||||
추출된 프로퍼티 딕셔너리
|
||||
"""
|
||||
custom_props = notify.get('CustomProperties', {})
|
||||
result = {}
|
||||
|
||||
for field in target_fields:
|
||||
if field in custom_props:
|
||||
value = custom_props[field]
|
||||
parsed = parse_custom_property(value, field)
|
||||
if parsed is not None:
|
||||
result[field] = parsed
|
||||
|
||||
return result
|
||||
377
데이터수집/수집스크립트/data_extractors.py
Normal file
377
데이터수집/수집스크립트/data_extractors.py
Normal file
@ -0,0 +1,377 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DataTable 및 AnimMontage 데이터 추출 모듈
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Any
|
||||
from custom_property_parser import extract_notify_properties, parse_custom_property
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 수집 대상 스토커 목록 (소문자)
|
||||
TARGET_STALKERS = [
|
||||
'hilda', 'urud', 'nave', 'baran', 'rio', 'clad',
|
||||
'rene', 'sinobu', 'lian', 'cazimord', 'blackmaria'
|
||||
]
|
||||
|
||||
|
||||
def round_float(value: Any) -> Any:
|
||||
"""소수점 2자리로 반올림 (float만 처리)"""
|
||||
if isinstance(value, (int, float)):
|
||||
return round(float(value), 2)
|
||||
return value
|
||||
|
||||
|
||||
def extract_character_stat(data_table_assets: List[Dict], stalker_name: str) -> Dict:
|
||||
"""
|
||||
DT_CharacterStat에서 스토커의 기본 스탯 추출
|
||||
|
||||
Args:
|
||||
data_table_assets: DataTable.json의 Assets 배열
|
||||
stalker_name: 스토커 이름 (소문자)
|
||||
|
||||
Returns:
|
||||
스탯 딕셔너리
|
||||
"""
|
||||
# DT_CharacterStat 찾기
|
||||
dt_char_stat = None
|
||||
for asset in data_table_assets:
|
||||
if asset.get('AssetName') == 'DT_CharacterStat':
|
||||
dt_char_stat = asset
|
||||
break
|
||||
|
||||
if not dt_char_stat:
|
||||
logger.warning("DT_CharacterStat not found")
|
||||
return {}
|
||||
|
||||
# 해당 스토커 행 찾기
|
||||
rows = dt_char_stat.get('Rows', [])
|
||||
stalker_row = None
|
||||
for row in rows:
|
||||
if row.get('RowName', '').lower() == stalker_name.lower():
|
||||
stalker_row = row
|
||||
break
|
||||
|
||||
if not stalker_row:
|
||||
logger.warning(f"Stalker '{stalker_name}' not found in DT_CharacterStat")
|
||||
return {}
|
||||
|
||||
data = stalker_row.get('Data', {})
|
||||
|
||||
# 모든 숫자 필드를 소수점 2자리로 반올림
|
||||
stats = {
|
||||
'name': data.get('name', ''),
|
||||
'jobName': data.get('jobName', ''),
|
||||
# 기본 스탯
|
||||
'str': round_float(data.get('str', 0)),
|
||||
'dex': round_float(data.get('dex', 0)),
|
||||
'int': round_float(data.get('int', 0)),
|
||||
'con': round_float(data.get('con', 0)),
|
||||
'wis': round_float(data.get('wis', 0)),
|
||||
# 체력/마나
|
||||
'hp': round_float(data.get('hP', 0)),
|
||||
'mp': round_float(data.get('mP', 0)),
|
||||
'manaRegen': round_float(data.get('manaRegen', 0)),
|
||||
'stamina': round_float(data.get('stamina', 0)),
|
||||
# 공격
|
||||
'physicalDamage': round_float(data.get('physicalDamage', 0)),
|
||||
'magicalDamage': round_float(data.get('magicalDamage', 0)),
|
||||
'criticalPer': round_float(data.get('criticalPer', 0)),
|
||||
'criticalDamage': round_float(data.get('criticalDamage', 0)),
|
||||
'backAttackDamage': round_float(data.get('backAttackDamage', 0)),
|
||||
# 방어
|
||||
'defense': round_float(data.get('defense', 0)),
|
||||
'physicalResistancePer': round_float(data.get('physicalResistancePer', 0)),
|
||||
'rangedResistancePer': round_float(data.get('rangedResistancePer', 0)),
|
||||
'magicalResistancePer': round_float(data.get('magicalResistancePer', 0)),
|
||||
# 속성 저항
|
||||
'fireResistancePer': round_float(data.get('fireResistancePer', 0)),
|
||||
'poisonResistancePer': round_float(data.get('poisonResistancePer', 0)),
|
||||
'waterResistancePer': round_float(data.get('waterResistancePer', 0)),
|
||||
'lightningResistancePer': round_float(data.get('lightningResistancePer', 0)),
|
||||
'holyResistancePer': round_float(data.get('holyResistancePer', 0)),
|
||||
'darkResistancePer': round_float(data.get('darkResistancePer', 0)),
|
||||
'dotReduceRatePer': round_float(data.get('dOTReduceRatePer', 0)),
|
||||
# 이동
|
||||
'walkSpeed': round_float(data.get('walkSpeed', 0)),
|
||||
# 스킬
|
||||
'defaultSkills': data.get('defaultSkills', []),
|
||||
'subSkill': data.get('subSkill', ''),
|
||||
'ultimateSkill': data.get('ultimateSkill', ''),
|
||||
'ultimatePoint': data.get('ultimatePoint', 0),
|
||||
# 장비
|
||||
'equipableTypes': data.get('equipableTypes', []),
|
||||
# 기타
|
||||
'hitRadius': round_float(data.get('hitRadius', 0))
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def extract_attack_montages(data_table_assets: List[Dict], stalker_name: str) -> Dict[str, List[str]]:
|
||||
"""
|
||||
DT_CharacterAbility에서 기본 공격 몽타주 추출
|
||||
|
||||
Args:
|
||||
data_table_assets: DataTable.json의 Assets 배열
|
||||
stalker_name: 스토커 이름 (소문자)
|
||||
|
||||
Returns:
|
||||
무기타입별 몽타주 경로 리스트 딕셔너리
|
||||
"""
|
||||
# DT_CharacterAbility 찾기
|
||||
dt_char_ability = None
|
||||
for asset in data_table_assets:
|
||||
if asset.get('AssetName') == 'DT_CharacterAbility':
|
||||
dt_char_ability = asset
|
||||
break
|
||||
|
||||
if not dt_char_ability:
|
||||
logger.warning("DT_CharacterAbility not found")
|
||||
return {}
|
||||
|
||||
# 해당 스토커 행 찾기
|
||||
rows = dt_char_ability.get('Rows', [])
|
||||
stalker_row = None
|
||||
for row in rows:
|
||||
if row.get('RowName', '').lower() == stalker_name.lower():
|
||||
stalker_row = row
|
||||
break
|
||||
|
||||
if not stalker_row:
|
||||
logger.warning(f"Stalker '{stalker_name}' not found in DT_CharacterAbility")
|
||||
return {}
|
||||
|
||||
data = stalker_row.get('Data', {})
|
||||
attack_montage_map = data.get('attackMontageMap', {})
|
||||
|
||||
# 무기타입별 몽타주 배열 추출
|
||||
result = {}
|
||||
for weapon_type, weapon_data in attack_montage_map.items():
|
||||
if isinstance(weapon_data, dict):
|
||||
montage_array = weapon_data.get('montageArray', [])
|
||||
if montage_array:
|
||||
result[weapon_type] = montage_array
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def should_exclude_skill_montage(montage_path: str) -> bool:
|
||||
"""
|
||||
스킬 몽타주 제외 규칙 적용
|
||||
|
||||
제외 키워드: ready, Equip, Equipment, _E
|
||||
"""
|
||||
if not montage_path:
|
||||
return True
|
||||
|
||||
exclude_keywords = ['ready', 'Equip', 'Equipment', '_E']
|
||||
montage_name = montage_path.split('/')[-1].split('.')[0]
|
||||
|
||||
for keyword in exclude_keywords:
|
||||
if keyword in montage_name:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def extract_skill_data(data_table_assets: List[Dict], skill_id: str) -> Optional[Dict]:
|
||||
"""
|
||||
DT_Skill에서 스킬 데이터 추출
|
||||
|
||||
Args:
|
||||
data_table_assets: DataTable.json의 Assets 배열
|
||||
skill_id: 스킬 ID (예: SK100201)
|
||||
|
||||
Returns:
|
||||
스킬 데이터 딕셔너리
|
||||
"""
|
||||
# DT_Skill 찾기
|
||||
dt_skill = None
|
||||
for asset in data_table_assets:
|
||||
if asset.get('AssetName') == 'DT_Skill':
|
||||
dt_skill = asset
|
||||
break
|
||||
|
||||
if not dt_skill:
|
||||
logger.warning("DT_Skill not found")
|
||||
return None
|
||||
|
||||
# 해당 스킬 행 찾기
|
||||
rows = dt_skill.get('Rows', [])
|
||||
skill_row = None
|
||||
for row in rows:
|
||||
if row.get('RowName') == skill_id:
|
||||
skill_row = row
|
||||
break
|
||||
|
||||
if not skill_row:
|
||||
logger.warning(f"Skill '{skill_id}' not found in DT_Skill")
|
||||
return None
|
||||
|
||||
data = skill_row.get('Data', {})
|
||||
|
||||
# 스킬 몽타주 필터링 (제외 규칙 적용)
|
||||
use_montages = data.get('useMontages', [])
|
||||
filtered_montages = [m for m in use_montages if not should_exclude_skill_montage(m)]
|
||||
|
||||
# GameplayEffect 정보 추출 (trigger와 gEClass만)
|
||||
gameplay_effects = []
|
||||
for ge in data.get('gameplayEffectSet', []):
|
||||
if isinstance(ge, dict):
|
||||
gameplay_effects.append({
|
||||
'trigger': ge.get('trigger', ''),
|
||||
'gEClass': ge.get('gEClass', '')
|
||||
})
|
||||
|
||||
skill_data = {
|
||||
'skillId': skill_id,
|
||||
'name': data.get('name', ''),
|
||||
'desc': data.get('desc', ''),
|
||||
'descValues': data.get('descValues', []),
|
||||
'simpleDesc': data.get('simpleDesc', ''),
|
||||
'skillAttackType': data.get('skillAttackType', ''),
|
||||
'skillElementType': data.get('skillElementType', ''),
|
||||
'skillDamageRate': data.get('skillDamageRate', 1.0),
|
||||
'walkSpeedMultiplier': data.get('walkSpeedMultiplier', 0),
|
||||
'castingTime': data.get('castingTime', 0),
|
||||
'manaCost': data.get('manaCost', 0),
|
||||
'coolTime': data.get('coolTime', 0),
|
||||
'useMontages': filtered_montages,
|
||||
'bIsStackable': data.get('bIsStackable', False),
|
||||
'maxStackCount': data.get('maxStackCount', 0),
|
||||
'bIsUltimate': data.get('bIsUltimate', False),
|
||||
'abilityClass': data.get('abilityClass', ''),
|
||||
'activeAbilityClass': data.get('activeAbilityClass', ''),
|
||||
'activeDuration': data.get('activeDuration', 0),
|
||||
'gameplayEffectSet': gameplay_effects
|
||||
}
|
||||
|
||||
return skill_data
|
||||
|
||||
|
||||
def extract_montage_asset_name(montage_path: str) -> str:
|
||||
"""
|
||||
몽타주 경로에서 에셋 이름 추출
|
||||
|
||||
Example:
|
||||
'/Script/Engine.AnimMontage'/Game/.../AM_PC_Hilda_B_Attack_W01_01.AM_PC_Hilda_B_Attack_W01_01''
|
||||
-> 'AM_PC_Hilda_B_Attack_W01_01'
|
||||
"""
|
||||
if not montage_path:
|
||||
return ''
|
||||
|
||||
# 마지막 .과 마지막 ' 사이의 문자열 추출
|
||||
parts = montage_path.split('.')
|
||||
if len(parts) >= 2:
|
||||
asset_name = parts[-1].rstrip("'")
|
||||
return asset_name
|
||||
|
||||
return montage_path
|
||||
|
||||
|
||||
def extract_anim_notifies(montage_data: Dict) -> List[Dict]:
|
||||
"""
|
||||
AnimMontage에서 주요 AnimNotifies 추출
|
||||
|
||||
수집 대상:
|
||||
- ANS_AttackState_C
|
||||
- AnimNotifyState_AttackWithEquip
|
||||
- ANS_SkillCancel_C
|
||||
- AN_Trigger_Projectile_Shot_C
|
||||
|
||||
Args:
|
||||
montage_data: AnimMontage 에셋 딕셔너리
|
||||
|
||||
Returns:
|
||||
추출된 AnimNotifies 리스트
|
||||
"""
|
||||
target_classes = {
|
||||
'ANS_AttackState_C': ['AddNormalAttackPer', 'AddPhysicalAttackPer'],
|
||||
'AnimNotifyState_AttackWithEquip': ['AttackTag'],
|
||||
'ANS_SkillCancel_C': [],
|
||||
'AN_Trigger_Projectile_Shot_C': ['EventTag']
|
||||
}
|
||||
|
||||
anim_notifies = montage_data.get('AnimNotifies', [])
|
||||
result = []
|
||||
|
||||
for notify in anim_notifies:
|
||||
notify_class = notify.get('NotifyStateClass') or notify.get('NotifyClass')
|
||||
|
||||
if notify_class in target_classes:
|
||||
extracted = {
|
||||
'notifyClass': notify_class,
|
||||
'triggerTime': notify.get('TriggerTime', 0),
|
||||
'duration': notify.get('Duration', 0)
|
||||
}
|
||||
|
||||
# CustomProperties에서 필요한 필드 추출
|
||||
target_fields = target_classes[notify_class]
|
||||
if target_fields:
|
||||
props = extract_notify_properties(notify, target_fields)
|
||||
extracted['properties'] = props
|
||||
|
||||
result.append(extracted)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def find_montage_data(anim_montage_assets: List[Dict], montage_path: str) -> Optional[Dict]:
|
||||
"""
|
||||
몽타주 경로로 AnimMontage 에셋 찾기
|
||||
|
||||
Args:
|
||||
anim_montage_assets: AnimMontage.json의 Assets 배열
|
||||
montage_path: 몽타주 경로
|
||||
|
||||
Returns:
|
||||
찾은 몽타주 데이터 또는 None
|
||||
"""
|
||||
asset_name = extract_montage_asset_name(montage_path)
|
||||
if not asset_name:
|
||||
return None
|
||||
|
||||
for montage in anim_montage_assets:
|
||||
if montage.get('AssetName') == asset_name:
|
||||
return montage
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_montage_info(anim_montage_assets: List[Dict], montage_path: str) -> Optional[Dict]:
|
||||
"""
|
||||
몽타주 경로로 몽타주 정보 추출 (AnimNotifies 포함)
|
||||
|
||||
Args:
|
||||
anim_montage_assets: AnimMontage.json의 Assets 배열
|
||||
montage_path: 몽타주 경로
|
||||
|
||||
Returns:
|
||||
몽타주 정보 딕셔너리
|
||||
"""
|
||||
montage_data = find_montage_data(anim_montage_assets, montage_path)
|
||||
if not montage_data:
|
||||
asset_name = extract_montage_asset_name(montage_path)
|
||||
logger.warning(f"Montage not found: {asset_name}")
|
||||
return None
|
||||
|
||||
sections = montage_data.get('Sections', [])
|
||||
section_info = []
|
||||
for section in sections:
|
||||
section_info.append({
|
||||
'sectionName': section.get('SectionName', ''),
|
||||
'startTime': section.get('StartTime', 0)
|
||||
})
|
||||
|
||||
montage_info = {
|
||||
'assetName': montage_data.get('AssetName', ''),
|
||||
'sequenceLength': montage_data.get('SequenceLength', 0),
|
||||
'rateScale': montage_data.get('RateScale', 1.0),
|
||||
'sections': section_info,
|
||||
'animNotifies': extract_anim_notifies(montage_data)
|
||||
}
|
||||
|
||||
return montage_info
|
||||
339
데이터수집/수집스크립트/markdown_formatter.py
Normal file
339
데이터수집/수집스크립트/markdown_formatter.py
Normal file
@ -0,0 +1,339 @@
|
||||
# -*- 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)
|
||||
61
데이터수집/수집스크립트/validate_result.py
Normal file
61
데이터수집/수집스크립트/validate_result.py
Normal file
@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""결과 검증 스크립트"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
JSON_FILE = os.path.join(BASE_DIR, '수집결과', 'all_stalkers_combat_data.json')
|
||||
|
||||
with open(JSON_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
hilda = data['stalkers']['hilda']
|
||||
|
||||
print('=== Hilda 기본 정보 ===')
|
||||
print(f"이름: {hilda['basic_info']['name']}")
|
||||
print(f"직업: {hilda['basic_info']['jobName']}")
|
||||
|
||||
print(f"\n=== 기본 스탯 (일부) ===")
|
||||
stats = hilda['stats']
|
||||
print(f"Str: {stats['str']}, Dex: {stats['dex']}, Int: {stats['int']}")
|
||||
print(f"스탯 합계: {stats['str'] + stats['dex'] + stats['int'] + stats['con'] + stats['wis']}")
|
||||
print(f"크리티컬: {stats['criticalPer']}%")
|
||||
|
||||
print(f"\n=== 스킬 ===")
|
||||
skills = hilda['skills']
|
||||
print(f"기본 스킬: {skills['default']}")
|
||||
print(f"서브 스킬: {skills['sub']}")
|
||||
print(f"궁극기: {skills['ultimate']}")
|
||||
|
||||
skill_201 = hilda['skill_details']['SK100201']
|
||||
print(f"\n=== SK100201 정보 ===")
|
||||
print(f"이름: {skill_201['name']}")
|
||||
print(f"설명: {skill_201['desc']}")
|
||||
print(f"descValues: {skill_201['descValues']}")
|
||||
print(f"피해 배율: {skill_201['skillDamageRate']}")
|
||||
print(f"몽타주 수: {len(skill_201['montages'])}")
|
||||
|
||||
if skill_201['montages']:
|
||||
montage = skill_201['montages'][0]
|
||||
print(f"첫 몽타주: {montage['assetName']}")
|
||||
print(f" - 시퀀스 길이: {montage['sequenceLength']}초")
|
||||
print(f" - AnimNotifies 수: {len(montage['animNotifies'])}")
|
||||
|
||||
if montage['animNotifies']:
|
||||
print(f"\n 첫 번째 AnimNotify:")
|
||||
notify = montage['animNotifies'][0]
|
||||
print(f" - Class: {notify['notifyClass']}")
|
||||
print(f" - TriggerTime: {notify['triggerTime']}초")
|
||||
print(f" - Duration: {notify['duration']}초")
|
||||
if 'properties' in notify:
|
||||
print(f" - Properties: {notify['properties']}")
|
||||
|
||||
print(f"\n=== 기본 공격 ===")
|
||||
basic_attacks = hilda['basic_attacks']
|
||||
for weapon_type, montages in basic_attacks.items():
|
||||
print(f"{weapon_type}: {len(montages)}개 몽타주")
|
||||
if montages:
|
||||
first = montages[0]
|
||||
print(f" - {first['assetName']}")
|
||||
print(f" - AnimNotifies: {len(first['animNotifies'])}개")
|
||||
176
데이터수집/수집스크립트/validators.py
Normal file
176
데이터수집/수집스크립트/validators.py
Normal file
@ -0,0 +1,176 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
데이터 검증 모듈
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_stalker_data(stalker_name: str, stalker_data: Dict) -> bool:
|
||||
"""
|
||||
스토커 데이터 검증
|
||||
|
||||
Args:
|
||||
stalker_name: 스토커 이름
|
||||
stalker_data: 스토커 데이터
|
||||
|
||||
Returns:
|
||||
검증 성공 여부
|
||||
"""
|
||||
warnings = []
|
||||
errors = []
|
||||
|
||||
# 기본 정보 검증
|
||||
basic_info = stalker_data.get('basic_info', {})
|
||||
if not basic_info.get('name'):
|
||||
warnings.append(f"[{stalker_name}] 이름 정보 누락")
|
||||
|
||||
# 스탯 검증
|
||||
stats = stalker_data.get('stats', {})
|
||||
if not stats:
|
||||
errors.append(f"[{stalker_name}] 스탯 정보 누락")
|
||||
else:
|
||||
# 스탯 합계 확인 (75여야 함)
|
||||
stat_sum = (
|
||||
stats.get('str', 0) +
|
||||
stats.get('dex', 0) +
|
||||
stats.get('int', 0) +
|
||||
stats.get('con', 0) +
|
||||
stats.get('wis', 0)
|
||||
)
|
||||
if stat_sum != 75:
|
||||
warnings.append(f"[{stalker_name}] 스탯 합계가 75가 아님: {stat_sum}")
|
||||
|
||||
# 스킬 검증
|
||||
skills = stalker_data.get('skills', {})
|
||||
default_skills = skills.get('default', [])
|
||||
if not default_skills:
|
||||
warnings.append(f"[{stalker_name}] 기본 스킬 정보 누락")
|
||||
|
||||
sub_skill = skills.get('sub', '')
|
||||
if not sub_skill:
|
||||
warnings.append(f"[{stalker_name}] 서브 스킬 정보 누락")
|
||||
|
||||
ultimate_skill = skills.get('ultimate', '')
|
||||
if not ultimate_skill:
|
||||
warnings.append(f"[{stalker_name}] 궁극기 정보 누락")
|
||||
|
||||
# 스킬 상세 정보 검증
|
||||
skill_details = stalker_data.get('skill_details', {})
|
||||
all_skill_ids = default_skills + [sub_skill, ultimate_skill]
|
||||
for skill_id in all_skill_ids:
|
||||
if skill_id and skill_id not in skill_details:
|
||||
warnings.append(f"[{stalker_name}] 스킬 상세 정보 누락: {skill_id}")
|
||||
|
||||
# 기본 공격 검증
|
||||
basic_attacks = stalker_data.get('basic_attacks', {})
|
||||
if not basic_attacks:
|
||||
warnings.append(f"[{stalker_name}] 기본 공격 몽타주 정보 누락")
|
||||
|
||||
# 경고 및 에러 출력
|
||||
for warning in warnings:
|
||||
logger.warning(warning)
|
||||
|
||||
for error in errors:
|
||||
logger.error(error)
|
||||
|
||||
return len(errors) == 0
|
||||
|
||||
|
||||
def validate_collection_result(collection_data: Dict) -> bool:
|
||||
"""
|
||||
전체 수집 결과 검증
|
||||
|
||||
Args:
|
||||
collection_data: 전체 수집 데이터
|
||||
|
||||
Returns:
|
||||
검증 성공 여부
|
||||
"""
|
||||
stalkers = collection_data.get('stalkers', {})
|
||||
|
||||
if not stalkers:
|
||||
logger.error("수집된 스토커 데이터가 없습니다")
|
||||
return False
|
||||
|
||||
total_stalkers = len(stalkers)
|
||||
logger.info(f"총 {total_stalkers}명의 스토커 데이터 수집 완료")
|
||||
|
||||
# 각 스토커별 검증
|
||||
all_valid = True
|
||||
for stalker_name, stalker_data in stalkers.items():
|
||||
if not validate_stalker_data(stalker_name, stalker_data):
|
||||
all_valid = False
|
||||
|
||||
return all_valid
|
||||
|
||||
|
||||
def log_collection_summary(collection_data: Dict, log_file_path: str):
|
||||
"""
|
||||
수집 결과 요약 로그 생성
|
||||
|
||||
Args:
|
||||
collection_data: 전체 수집 데이터
|
||||
log_file_path: 로그 파일 경로
|
||||
"""
|
||||
stalkers = collection_data.get('stalkers', {})
|
||||
|
||||
with open(log_file_path, 'w', encoding='utf-8') as f:
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write("DS 전투 데이터 수집 결과 요약\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
|
||||
metadata = collection_data.get('collection_metadata', {})
|
||||
f.write(f"수집 시각: {metadata.get('collected_at', '')}\n")
|
||||
f.write(f"총 스토커 수: {len(stalkers)}\n\n")
|
||||
|
||||
for stalker_name, stalker_data in stalkers.items():
|
||||
f.write("-" * 80 + "\n")
|
||||
f.write(f"스토커: {stalker_name}\n")
|
||||
f.write("-" * 80 + "\n")
|
||||
|
||||
basic_info = stalker_data.get('basic_info', {})
|
||||
f.write(f" 이름: {basic_info.get('name', '')}\n")
|
||||
f.write(f" 직업: {basic_info.get('jobName', '')}\n")
|
||||
|
||||
stats = stalker_data.get('stats', {})
|
||||
stat_sum = (
|
||||
stats.get('str', 0) +
|
||||
stats.get('dex', 0) +
|
||||
stats.get('int', 0) +
|
||||
stats.get('con', 0) +
|
||||
stats.get('wis', 0)
|
||||
)
|
||||
f.write(f" 스탯 합계: {stat_sum}\n")
|
||||
|
||||
skills = stalker_data.get('skills', {})
|
||||
default_skills = skills.get('default', [])
|
||||
f.write(f" 기본 스킬 수: {len(default_skills)}\n")
|
||||
f.write(f" 서브 스킬: {skills.get('sub', '')}\n")
|
||||
f.write(f" 궁극기: {skills.get('ultimate', '')}\n")
|
||||
|
||||
skill_details = stalker_data.get('skill_details', {})
|
||||
f.write(f" 수집된 스킬 상세 정보 수: {len(skill_details)}\n")
|
||||
|
||||
# 각 스킬별 몽타주 수 집계
|
||||
total_skill_montages = 0
|
||||
for skill_id, skill_data in skill_details.items():
|
||||
montages = skill_data.get('montages', [])
|
||||
total_skill_montages += len(montages)
|
||||
|
||||
f.write(f" 스킬 몽타주 총 수: {total_skill_montages}\n")
|
||||
|
||||
basic_attacks = stalker_data.get('basic_attacks', {})
|
||||
total_basic_montages = sum(len(montages) for montages in basic_attacks.values())
|
||||
f.write(f" 기본 공격 몽타주 총 수: {total_basic_montages}\n")
|
||||
|
||||
f.write("\n")
|
||||
|
||||
f.write("=" * 80 + "\n")
|
||||
f.write("수집 완료\n")
|
||||
f.write("=" * 80 + "\n")
|
||||
|
||||
logger.info(f"수집 요약 로그 생성 완료: {log_file_path}")
|
||||
Reference in New Issue
Block a user