20251117_104033 기준 데이터
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user