Files
DS-Combat_analy/legacy/분석도구/v2/validate_stalker_data.py
2025-11-05 11:09:16 +09:00

357 lines
14 KiB
Python

#!/usr/bin/env python3
"""
스토커 데이터 검증 스크립트 v2
intermediate_data.json의 데이터 정확성을 교차 검증
"""
import json
import sys
from pathlib import Path
from typing import Dict, List, Tuple
# config 임포트
sys.path.append(str(Path(__file__).parent))
import config
class ValidationReport:
"""검증 리포트"""
def __init__(self):
self.passes = []
self.warnings = []
self.failures = []
def add_pass(self, message: str):
self.passes.append(f"[PASS] {message}")
def add_warning(self, message: str):
self.warnings.append(f"[WARN] {message}")
def add_failure(self, message: str):
self.failures.append(f"[FAIL] {message}")
def print_summary(self):
"""요약 출력"""
print("\n" + "="*80)
print("검증 리포트 요약")
print("="*80)
print(f"[PASS] 통과: {len(self.passes)}")
print(f"[WARN] 경고: {len(self.warnings)}")
print(f"[FAIL] 실패: {len(self.failures)}")
total = len(self.passes) + len(self.warnings) + len(self.failures)
if total > 0:
confidence = (len(self.passes) / total) * 100
print(f"[INFO] 데이터 신뢰도: {confidence:.1f}%")
def print_details(self):
"""상세 출력"""
if self.failures:
print("\n[ 실패 항목 ]")
for fail in self.failures:
print(fail)
if self.warnings:
print("\n[ 경고 항목 ]")
for warn in self.warnings:
print(warn)
if self.passes and len(self.passes) <= 20:
print("\n[ 통과 항목 (샘플) ]")
for pass_msg in self.passes[:10]:
print(pass_msg)
def to_markdown(self, output_path: Path):
"""마크다운 파일로 저장"""
with open(output_path, 'w', encoding='utf-8') as f:
f.write("# 데이터 검증 리포트\n\n")
f.write(f"**생성 시각**: {Path(output_path).stat().st_mtime}\n\n")
f.write("## 전체 요약\n\n")
f.write(f"- ✅ 검증 통과: **{len(self.passes)}개** 항목\n")
f.write(f"- ⚠️ 경고: **{len(self.warnings)}개** 항목\n")
f.write(f"- ❌ 실패: **{len(self.failures)}개** 항목\n")
total = len(self.passes) + len(self.warnings) + len(self.failures)
if total > 0:
confidence = (len(self.passes) / total) * 100
f.write(f"- 📊 데이터 신뢰도: **{confidence:.1f}%**\n\n")
if self.failures:
f.write("## ❌ 실패 항목\n\n")
for fail in self.failures:
f.write(f"{fail}\n\n")
if self.warnings:
f.write("## ⚠️ 경고 항목\n\n")
for warn in self.warnings:
f.write(f"{warn}\n\n")
f.write("## ✅ 통과 항목\n\n")
f.write(f"{len(self.passes)}개 항목이 검증을 통과했습니다.\n\n")
def validate_stalker_count(data: Dict, report: ValidationReport):
"""스토커 수 검증"""
expected_count = len(config.STALKERS)
actual_count = len(data)
if actual_count == expected_count:
report.add_pass(f"스토커 수 일치 ({actual_count}명)")
else:
report.add_failure(f"스토커 수 불일치 (예상:{expected_count}, 실제:{actual_count})")
def validate_stalker_stats(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""스토커 기본 스탯 검증"""
stats = stalker_data['stats']['stats']
# 스탯 합계
stat_sum = sum(stats.values())
expected_sum = config.VALIDATION_RULES['stat_total']
if stat_sum == expected_sum:
report.add_pass(f"{stalker_id}: 스탯 합계 = {stat_sum}")
else:
report.add_failure(f"{stalker_id}: 스탯 합계 불일치 (예상:{expected_sum}, 실제:{stat_sum})")
# HP/MP 검증
hp = stalker_data['stats']['hp']
mp = stalker_data['stats']['mp']
if hp == config.VALIDATION_RULES['hp']:
report.add_pass(f"{stalker_id}: HP = {hp}")
else:
report.add_warning(f"{stalker_id}: HP 불일치 (예상:{config.VALIDATION_RULES['hp']}, 실제:{hp})")
if mp == config.VALIDATION_RULES['mp']:
report.add_pass(f"{stalker_id}: MP = {mp}")
else:
report.add_warning(f"{stalker_id}: MP 불일치 (예상:{config.VALIDATION_RULES['mp']}, 실제:{mp})")
def validate_skills(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""스킬 데이터 검증"""
skills = stalker_data['skills']
for skill_id, skill_data in skills.items():
# skillDamageRate 범위
rate = skill_data.get('skillDamageRate', 0)
if rate < 0:
report.add_failure(f"{stalker_id}/{skill_id}: skillDamageRate = {rate} (음수)")
# coolTime 범위
cooltime = skill_data.get('coolTime', 0)
if cooltime < 0:
report.add_failure(f"{stalker_id}/{skill_id}: coolTime = {cooltime} (음수)")
# 궁극기 체크
if skill_data.get('bIsUltimate', False):
ultimate_skill_id = stalker_data['stats']['ultimateSkill']
if skill_id == ultimate_skill_id:
report.add_pass(f"{stalker_id}: 궁극기 매칭 ({skill_id})")
else:
report.add_warning(f"{stalker_id}: 궁극기 ID 불일치 (스탯:{ultimate_skill_id}, 스킬:{skill_id})")
# 몽타주 연결 검증
use_montages = skill_data.get('useMontages', [])
montage_data = skill_data.get('montageData', [])
if len(use_montages) > 0 and len(montage_data) == 0:
report.add_warning(f"{stalker_id}/{skill_id}: 몽타주 경로는 있지만 데이터 없음")
# 유틸리티 스킬 판별 검증
is_utility = skill_data.get('isUtility', False)
has_attack = any(m.get('hasAttack', False) for m in montage_data) if montage_data else False
skill_rate = skill_data.get('skillDamageRate', 0)
if is_utility and has_attack and skill_rate > 0:
report.add_warning(f"{stalker_id}/{skill_id}: 유틸리티로 분류되었지만 공격 노티파이 있음")
def validate_summon_skills(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""소환수 스킬 검증"""
if stalker_id != 'rene':
return
skills = stalker_data['skills']
for skill_id in config.SUMMON_SKILLS.keys():
if skill_id not in skills:
report.add_failure(f"{stalker_id}: 소환수 스킬 {skill_id} 없음")
continue
skill_data = skills[skill_id]
# activeDuration 확인
duration = skill_data.get('activeDuration', 0)
if duration == 0:
report.add_failure(f"{stalker_id}/{skill_id}: 소환 지속시간 = 0")
else:
report.add_pass(f"{stalker_id}/{skill_id}: 소환 지속시간 = {duration}")
# 시전 몽타주 확인 (공격 노티파이 체크 제외 - 소환만 하기 때문)
montage_data = skill_data.get('montageData', [])
if montage_data:
for montage in montage_data:
seq_len = montage.get('sequenceLength', 0)
montage_name = montage.get('assetName', '')
if seq_len == 0:
report.add_failure(f"{stalker_id}/{skill_id}: 시전 몽타주 {montage_name} SequenceLength = 0")
else:
report.add_pass(f"{stalker_id}/{skill_id}: 시전 몽타주 {montage_name} = {seq_len:.2f}")
# 소환수 공격 몽타주 확인
summon_montage_data = skill_data.get('summonMontageData', [])
summon_type = skill_data.get('summonType', 'npc')
if not summon_montage_data:
report.add_warning(f"{stalker_id}/{skill_id}: 소환수 공격 몽타주 없음")
else:
for montage in summon_montage_data:
seq_len = montage.get('sequenceLength', 0)
has_attack = montage.get('hasAttack', False)
montage_name = montage.get('assetName', '')
attack_interval = montage.get('attackInterval', 0)
if seq_len == 0:
report.add_failure(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} SequenceLength = 0")
else:
if summon_type == 'special' and attack_interval > 0:
# Shiva: 공격 주기 표시
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} (주기: {attack_interval:.2f}초)")
else:
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} = {seq_len:.2f}")
# 소환수 공격 노티파이 확인
# NPC 소환수는 AnimNotify 외의 방식으로 피해를 입힐 수 있으므로 PASS 처리
if has_attack:
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} 공격 노티파이 확인")
else:
# 공격 노티파이 없어도 정상 (소환수는 다른 방식으로 피해 입힘)
report.add_pass(f"{stalker_id}/{skill_id}: 소환수 몽타주 {montage_name} (AnimNotify 방식 아님)")
def validate_dot_skills(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""DoT 스킬 검증"""
skills = stalker_data['skills']
for skill_id, dot_info in config.DOT_SKILLS.items():
if dot_info['stalker'] != stalker_id:
continue
if skill_id not in skills:
report.add_failure(f"{stalker_id}: DoT 스킬 {skill_id} 없음")
continue
skill_data = skills[skill_id]
is_dot = skill_data.get('isDot', False)
if is_dot:
report.add_pass(f"{stalker_id}/{skill_id}: DoT 스킬 마킹 확인")
else:
report.add_warning(f"{stalker_id}/{skill_id}: DoT 스킬이지만 마킹 안 됨")
def validate_blueprint_connections(stalker_id: str, stalker_data: Dict, report: ValidationReport):
"""Blueprint 연결 검증"""
skills = stalker_data['skills']
# 재장전 스킬은 Blueprint 변수 없어도 정상 (상수 사용)
reload_skills = ['SK110207', 'SK190209'] # urud, lian 재장전
ultimate_exceptions = ['SK170301'] # cazimord 궁극기
for skill_id, skill_data in skills.items():
ability_class = skill_data.get('abilityClass', '')
if not ability_class or ability_class == 'None':
continue
bp_vars = skill_data.get('blueprintVariables', {})
bp_name = ability_class.split('/')[-1].split('.')[0] if '/' in ability_class else ability_class
if not bp_vars:
# 재장전 스킬은 경고 제외
if skill_id in reload_skills:
report.add_pass(f"{stalker_id}/{skill_id}: 재장전 스킬 (Blueprint 변수 불필요)")
elif skill_id in ultimate_exceptions:
report.add_pass(f"{stalker_id}/{skill_id}: Blueprint 변수 없음 (정상 - {bp_name})")
else:
report.add_warning(f"{stalker_id}/{skill_id}: Blueprint '{bp_name}'에 변수 없음 (abilityClass: {ability_class})")
else:
# ActivationOrderGroup 확인
if 'ActivationOrderGroup' in bp_vars:
order_group = bp_vars['ActivationOrderGroup']['defaultValue']
report.add_pass(f"{stalker_id}/{skill_id}: ActivationOrderGroup = {order_group}")
else:
# 변수는 있지만 ActivationOrderGroup이 없음
var_names = list(bp_vars.keys())[:3] # 처음 3개 변수명만
report.add_pass(f"{stalker_id}/{skill_id}: Blueprint 변수 {len(bp_vars)}개 (예: {', '.join(var_names)})")
def main():
"""메인 실행 함수"""
print("="*80)
print("스토커 데이터 검증 v2")
print("="*80)
# 중간 데이터 로드
intermediate_file = config.OUTPUT_DIR / "intermediate_data.json"
if not intermediate_file.exists():
print(f"[FAIL] 중간 데이터 파일 없음: {intermediate_file}")
print("먼저 extract_stalker_data_v2.py를 실행하세요.")
return
print(f"\n[ 중간 데이터 로드 ]: {intermediate_file}")
with open(intermediate_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# 검증 실행
report = ValidationReport()
print("\n[ 검증 시작 ]")
# 1. 전체 스토커 수
print("\n1. 스토커 수 검증...")
validate_stalker_count(data, report)
# 2. 스토커별 검증
for stalker_id in config.STALKERS:
if stalker_id not in data:
report.add_failure(f"{stalker_id}: 데이터 없음")
continue
stalker_data = data[stalker_id]
print(f"\n2. {stalker_id} 검증...")
# 기본 스탯
validate_stalker_stats(stalker_id, stalker_data, report)
# 스킬
validate_skills(stalker_id, stalker_data, report)
# 소환수 (레네만)
validate_summon_skills(stalker_id, stalker_data, report)
# DoT
validate_dot_skills(stalker_id, stalker_data, report)
# Blueprint 연결
validate_blueprint_connections(stalker_id, stalker_data, report)
# 3. 결과 출력
report.print_summary()
report.print_details()
# 4. 마크다운 저장
report_file = config.OUTPUT_DIR / "검증_리포트.md"
report.to_markdown(report_file)
print(f"\n[OK] 검증 리포트 저장: {report_file}")
# 5. 검증 데이터 저장 (통과한 데이터만)
if len(report.failures) == 0:
validated_file = config.OUTPUT_DIR / "validated_data.json"
with open(validated_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"[OK] 검증된 데이터 저장: {validated_file}")
else:
print(f"\n[WARN] 실패 항목이 있어 validated_data.json을 생성하지 않았습니다.")
print(f" intermediate_data.json은 그대로 유지됩니다.")
if __name__ == "__main__":
main()