v2 폐기하고 v3로 새출발
This commit is contained in:
356
legacy/분석도구/v2/validate_stalker_data.py
Normal file
356
legacy/분석도구/v2/validate_stalker_data.py
Normal file
@ -0,0 +1,356 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user