Files

378 lines
12 KiB
Python
Raw Permalink Normal View History

2025-11-17 16:56:36 +09:00
# -*- 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