분석 v2.1
This commit is contained in:
@ -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. 결과 저장 (새 디렉토리 생성)
|
||||
|
||||
Reference in New Issue
Block a user