분석 v2.1

This commit is contained in:
Gnill82
2025-10-28 12:34:12 +09:00
parent ee1900f268
commit a354adf371
17 changed files with 415324 additions and 334641 deletions

View File

@ -278,21 +278,40 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
"""
AnimMontage.json에서 몽타주 타이밍 및 노티파이 추출
- AddNormalAttackPer 추출 (ANS_AttackState_C 노티파이)
- cancellableTime 추출 (ANS_DisableBlockingState_C 노티파이)
Returns:
{montage_name: {timing, notifies, attackMultiplier}}
{montage_name: {timing, notifies, attackMultiplier, cancellableTime}}
"""
print("\n=== AnimMontage 추출 ===")
all_montages = {}
pc_montages = [m for m in montages if 'AM_PC_' in m.get('AssetName', '') or 'AM_Sum_' in m.get('AssetName', '')]
# 콤보 캔슬 적용 대상 스토커 및 패턴 (평타만 해당)
CANCEL_TARGETS = {
'hilda': ['AM_PC_Hilda_B_Attack_W01_'], # weaponShield
'baran': ['AM_PC_Baran_B_Attack_W01_'], # twoHandWeapon
'clad': ['AM_PC_Clad_Base_Attack_Mace'] # oneHandWeapon (mace) - 특수 패턴
}
for montage in pc_montages:
asset_name = montage['AssetName']
# 공격 노티파이 추출
attack_notifies = []
attack_multiplier = 0.0 # AddNormalAttackPer (기본값 0)
cancellable_time = None # 콤보 캔슬 가능 시간 (기본값 None)
# 콤보 캔슬 적용 대상 판별
is_cancel_target = False
for stalker_name, patterns in CANCEL_TARGETS.items():
for pattern in patterns:
if pattern in asset_name:
is_cancel_target = True
break
if is_cancel_target:
break
for notify in montage.get('AnimNotifies', []):
notify_class = notify.get('NotifyClass', '')
@ -308,6 +327,12 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
except (ValueError, TypeError):
attack_multiplier = 0.0
# ANS_DisableBlockingState_C에서 콤보 캔슬 시간 추출 (적용 대상만)
if is_cancel_target and 'ANS_DisableBlockingState' in notify_state_class:
trigger_time = notify.get('TriggerTime', 0)
duration = notify.get('Duration', 0)
cancellable_time = trigger_time + duration
# 공격 판정 로직 (우선순위)
is_attack_notify = False
@ -354,6 +379,7 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
'sequenceLength': seq_len,
'rateScale': rate_scale,
'actualDuration': actual_duration, # 시퀀스 길이 (SequenceLength / RateScale)
'cancellableTime': cancellable_time, # 콤보 캔슬 가능 시간 (해당되는 경우만)
'attackMultiplier': attack_multiplier, # AddNormalAttackPer
'sections': montage.get('Sections', []),
'numSections': montage.get('NumSections', 0),
@ -367,6 +393,15 @@ def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
print(f" [OK] 총 {len(all_montages)}개 몽타주 추출 (PC + Summon)")
# 콤보 캔슬 적용된 몽타주 확인
cancel_montages = [(name, data['cancellableTime'], data['actualDuration'])
for name, data in all_montages.items()
if data.get('cancellableTime') is not None]
if cancel_montages:
print(f" [INFO] 콤보 캔슬 적용 몽타주: {len(cancel_montages)}")
for name, cancel_time, actual_time in cancel_montages:
print(f" - {name}: 캔슬 {cancel_time:.2f}초 (원본 {actual_time:.2f}초)")
# 소환수 몽타주 확인
summon_montages = [m for m in all_montages.keys() if 'Summon' in m or 'Sum_' in m]
if summon_montages:
@ -419,13 +454,165 @@ def extract_npc_abilities(datatables: List[Dict]) -> Dict[str, Dict]:
return npc_abilities
def extract_runes(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_Rune에서 룬 데이터 추출
Returns:
{runeId: {runeSet, level, name, desc, attributeModifies, ...}}
"""
print("\n=== DT_Rune 추출 ===")
rune_table = find_table(datatables, 'DT_Rune')
if not rune_table:
print("[WARN] DT_Rune 테이블을 찾을 수 없습니다.")
return {}
runes = {}
for row in rune_table.get('Rows', []):
rune_id = row['RowName']
data = row['Data']
# attributeModifies 파싱
attr_modifies = []
for mod in data.get('attributeModifies', []):
attr = mod.get('attribute', {})
attr_modifies.append({
'attributeName': attr.get('attributeName', ''),
'value': mod.get('value', 0)
})
runes[rune_id] = {
'runeId': rune_id,
'runeSet': data.get('runeSet', ''),
'level': data.get('level', 1),
'name': data.get('runeName', ''),
'desc': format_description(data.get('desc', ''), data.get('descValue', [])),
'descValue': data.get('descValue', []),
'attributeModifies': attr_modifies,
'unlockGold': data.get('unlockGold', 0),
'unlockSkillPoint': data.get('unlockSkillPoint', 0)
}
print(f" [OK] {len(runes)}개 룬 데이터 추출 완료")
return runes
def extract_rune_groups(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_RuneGroup에서 룬 그룹 데이터 추출
Returns:
{groupId: {name, type, coreLine, sub1Line, sub2Line}}
"""
print("\n=== DT_RuneGroup 추출 ===")
rune_group_table = find_table(datatables, 'DT_RuneGroup')
if not rune_group_table:
print("[WARN] DT_RuneGroup 테이블을 찾을 수 없습니다.")
return {}
groups = {}
for row in rune_group_table.get('Rows', []):
group_id = row['RowName']
data = row['Data']
groups[group_id] = {
'groupId': group_id,
'name': data.get('name', ''),
'type': data.get('type', ''),
'coreLine': data.get('coreLine', []),
'sub1Line': data.get('sub1Line', []),
'sub2Line': data.get('sub2Line', [])
}
print(f" [OK] {data.get('name', group_id)}: Core({len(data.get('coreLine', []))}), Sub1({len(data.get('sub1Line', []))}), Sub2({len(data.get('sub2Line', []))})")
return groups
def extract_equipment(datatables: List[Dict]) -> Dict[str, Dict]:
"""
DT_Equip에서 장비 데이터 추출
Returns:
{equipId: {name, equipSlotType, equipType, rarity, stats, ...}}
"""
print("\n=== DT_Equip 추출 ===")
equip_table = find_table(datatables, 'DT_Equip')
if not equip_table:
print("[WARN] DT_Equip 테이블을 찾을 수 없습니다.")
return {}
equipment = {}
for row in equip_table.get('Rows', []):
equip_id = row['RowName']
data = row['Data']
# stats 파싱
stats = []
for stat in data.get('stats', []):
attr = stat.get('attribute', {})
stats.append({
'attributeName': attr.get('attributeName', ''),
'value': stat.get('value', 0),
'visible': stat.get('visible', False)
})
equipment[equip_id] = {
'equipId': equip_id,
'name': data.get('name', ''),
'desc': data.get('desc', ''),
'equipSlotType': data.get('equipSlotType', ''),
'equipType': data.get('equipType', ''),
'rarity': data.get('rarity', ''),
'price': data.get('price', 0),
'sellPrice': data.get('sellPrice', 0),
'stats': stats,
'armor': data.get('armor', 0)
}
print(f" [OK] {len(equipment)}개 장비 데이터 추출 완료")
return equipment
def extract_float_constants(datatables: List[Dict]) -> Dict[str, float]:
"""
DT_Float에서 기어스코어 공식 상수 추출
Returns:
{constantName: value}
"""
print("\n=== DT_Float (기어스코어 상수) 추출 ===")
float_table = find_table(datatables, 'DT_Float')
if not float_table:
print("[WARN] DT_Float 테이블을 찾을 수 없습니다.")
return {}
constants = {}
gearscore_keys = [
'GearScoreEquipCommon',
'GearScoreEquipUncommon',
'GearScoreEquipRare',
'GearScoreEquipLegendary',
'GearScoreSkillPassive',
'GearScoreSkillPerk'
]
for row in float_table.get('Rows', []):
row_name = row['RowName']
if row_name in gearscore_keys:
constants[row_name] = row['Data'].get('value', 0)
print(f" [OK] {row_name}: {constants[row_name]}")
return constants
def organize_stalker_data(
stalker_stats: Dict,
stalker_abilities: Dict,
all_skills: Dict,
skill_blueprints: Dict,
anim_montages: Dict,
npc_abilities: Dict
npc_abilities: Dict,
runes: Dict,
rune_groups: Dict,
equipment: Dict,
float_constants: Dict
) -> Dict[str, Dict]:
"""
스토커별로 모든 데이터를 통합 정리
@ -479,6 +666,18 @@ def organize_stalker_data(
else:
print(f" [WARN] {skill_id}: 몽타주 {montage_name} 정보 없음")
# 바란 궁극기 특수 처리: AN_SimpleSendEvent 시점을 castingTime으로 사용
if skill_id == 'SK130301': # 바란 궁극기 '일격분쇄'
for montage_data in skill_data['montageData']:
for notify in montage_data.get('allNotifies', []):
if 'SimpleSendEvent' in notify.get('NotifyClass', ''):
event_tag = notify.get('CustomProperties', {}).get('Event Tag', '')
if 'Ability.Attack.Ready' in event_tag:
trigger_time = notify.get('TriggerTime', 0)
skill_data['castingTime'] = round(trigger_time, 2)
print(f" [INFO] {skill_id}: castingTime 오버라이드 {skill_data['castingTime']}초 (AN_SimpleSendEvent)")
break
# DoT 스킬 체크
skill_data['isDot'] = skill_id in config.DOT_SKILLS
@ -540,6 +739,7 @@ def organize_stalker_data(
'sequenceLength': montage_info['sequenceLength'],
'rateScale': montage_info['rateScale'],
'actualDuration': montage_info['actualDuration'],
'cancellableTime': montage_info.get('cancellableTime'), # 콤보 캔슬 시간 (해당되는 경우)
'attackMultiplier': montage_info['attackMultiplier'],
'hasAttack': montage_info['hasAttack']
})
@ -583,6 +783,14 @@ def organize_stalker_data(
skill_count = len(skills)
print(f" [OK] {stats['name']} ({stalker_id}): {skill_count}개 스킬")
# 공통 데이터 추가
organized['_metadata'] = {
'runes': runes,
'runeGroups': rune_groups,
'equipment': equipment,
'gearScoreConstants': float_constants
}
return organized
def is_utility_skill(skill_data: Dict) -> bool:
@ -643,6 +851,10 @@ def main():
skill_blueprints = extract_skill_blueprints(blueprints)
anim_montages = extract_anim_montages(montages)
npc_abilities = extract_npc_abilities(datatables) # 소환수 데이터
runes = extract_runes(datatables) # 룬 데이터
rune_groups = extract_rune_groups(datatables) # 룬 그룹 데이터
equipment = extract_equipment(datatables) # 장비 데이터
float_constants = extract_float_constants(datatables) # 기어스코어 상수
# 3. 데이터 통합
organized_data = organize_stalker_data(
@ -651,7 +863,11 @@ def main():
all_skills,
skill_blueprints,
anim_montages,
npc_abilities
npc_abilities,
runes,
rune_groups,
equipment,
float_constants
)
# 4. 결과 저장 (새 디렉토리 생성)