분석 v2.1
This commit is contained in:
Binary file not shown.
565
분석도구/v2/calculate_dps_scenarios_v2.py
Normal file
565
분석도구/v2/calculate_dps_scenarios_v2.py
Normal file
@ -0,0 +1,565 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
스토커 DPS 시나리오 계산 v2
|
||||
- 3개 시나리오 계산: 평타, 로테이션 (30초), 버스트 (10초)
|
||||
- 특수 상황 분석: DoT, 소환체, 패링
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Tuple, Any
|
||||
from config import (
|
||||
get_output_dir, STALKERS, STALKER_INFO,
|
||||
BASE_DAMAGE_FORMULA, DOT_SKILLS, DOT_DAMAGE,
|
||||
SUMMON_SKILLS, UTILITY_SKILLS, ANALYSIS_BASELINE
|
||||
)
|
||||
|
||||
|
||||
def load_validated_data(output_dir: Path) -> Dict:
|
||||
"""validated_data.json 또는 intermediate_data.json 로드"""
|
||||
validated_file = output_dir / "validated_data.json"
|
||||
intermediate_file = output_dir / "intermediate_data.json"
|
||||
|
||||
if validated_file.exists():
|
||||
data_file = validated_file
|
||||
print(f"Using validated_data.json")
|
||||
elif intermediate_file.exists():
|
||||
data_file = intermediate_file
|
||||
print(f"Using intermediate_data.json")
|
||||
else:
|
||||
raise FileNotFoundError(f"No data file found in {output_dir}")
|
||||
|
||||
with open(data_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def calculate_base_damage(stalker_id: str, stats: Dict) -> float:
|
||||
"""BaseDamage 계산 (Level 20, GearScore 400 기준)"""
|
||||
role = STALKER_INFO[stalker_id]['role']
|
||||
|
||||
# 주 스탯 결정 (실제 높은 스탯 또는 역할 기준)
|
||||
if stalker_id == 'hilda':
|
||||
# Hilda: STR 20 → Physical STR
|
||||
damage_type = 'physical_str'
|
||||
elif stalker_id == 'urud':
|
||||
# Urud: DEX 20 → Physical DEX
|
||||
damage_type = 'physical_dex'
|
||||
elif stalker_id == 'nave':
|
||||
# Nave: INT 25 → Magical
|
||||
damage_type = 'magical'
|
||||
elif stalker_id == 'baran':
|
||||
# Baran: STR 25 → Physical STR
|
||||
damage_type = 'physical_str'
|
||||
elif stalker_id == 'rio':
|
||||
# Rio: DEX 25 → Physical DEX
|
||||
damage_type = 'physical_dex'
|
||||
elif stalker_id == 'clad':
|
||||
# Clad: STR 15 (not WIS!) → Support
|
||||
damage_type = 'support'
|
||||
elif stalker_id == 'rene':
|
||||
# Rene: INT 20 → Magical
|
||||
damage_type = 'magical'
|
||||
elif stalker_id == 'sinobu':
|
||||
# Sinobu: DEX 25 → Physical DEX
|
||||
damage_type = 'physical_dex'
|
||||
elif stalker_id == 'lian':
|
||||
# Lian: DEX 20 → Physical DEX
|
||||
damage_type = 'physical_dex'
|
||||
elif stalker_id == 'cazimord':
|
||||
# Cazimord: DEX 25, STR 15 → Physical DEX
|
||||
damage_type = 'physical_dex'
|
||||
else:
|
||||
# Default fallback
|
||||
damage_type = 'physical_str'
|
||||
|
||||
return BASE_DAMAGE_FORMULA[damage_type](stats)
|
||||
|
||||
|
||||
def calculate_basic_attack_dps(stalker_id: str, stalker_data: Dict, base_damage: float) -> Dict:
|
||||
"""시나리오 1: 평타 DPS 계산"""
|
||||
basic_attacks = stalker_data.get('basicAttacks', {})
|
||||
|
||||
# 첫 번째 무기 타입의 평타 사용 (대부분 한 가지 무기만 사용)
|
||||
weapon_type = list(basic_attacks.keys())[0] if basic_attacks else None
|
||||
if not weapon_type:
|
||||
return {
|
||||
'dps': 0,
|
||||
'combo_time': 0,
|
||||
'total_multiplier': 0,
|
||||
'attacks': [],
|
||||
'notes': '평타 데이터 없음'
|
||||
}
|
||||
|
||||
attacks = basic_attacks[weapon_type]
|
||||
|
||||
# 총 콤보 시간 및 평타 배율 합계 계산
|
||||
combo_time = sum(atk['actualDuration'] for atk in attacks)
|
||||
|
||||
# attackMultiplier는 AddNormalAttackPer 값 (음수는 감소, 양수는 증가)
|
||||
# 실제 배율 = 1.0 + (attackMultiplier / 100)
|
||||
total_multiplier = sum(1.0 + (atk['attackMultiplier'] / 100.0) for atk in attacks)
|
||||
|
||||
# 평타 DPS 계산
|
||||
if combo_time > 0:
|
||||
basic_dps = (base_damage * total_multiplier) / combo_time
|
||||
else:
|
||||
basic_dps = 0
|
||||
|
||||
return {
|
||||
'dps': round(basic_dps, 2),
|
||||
'combo_time': round(combo_time, 2),
|
||||
'total_multiplier': round(total_multiplier, 2),
|
||||
'base_damage': round(base_damage, 2),
|
||||
'attacks': [
|
||||
{
|
||||
'index': atk['index'],
|
||||
'name': atk['montageName'],
|
||||
'duration': round(atk['actualDuration'], 2),
|
||||
'multiplier': round(1.0 + (atk['attackMultiplier'] / 100.0), 2)
|
||||
}
|
||||
for atk in attacks
|
||||
],
|
||||
'notes': ''
|
||||
}
|
||||
|
||||
|
||||
def calculate_skill_rotation_dps(stalker_id: str, stalker_data: Dict, base_damage: float, duration: float = 30.0) -> Dict:
|
||||
"""시나리오 2: 스킬 로테이션 DPS (30초 기본)"""
|
||||
skills = stalker_data.get('skills', {})
|
||||
stats = stalker_data['stats']
|
||||
|
||||
# 마나 회복 (0.2/초 + 룬 +70% = 0.34/초)
|
||||
mana_regen_rate = stats.get('manaRegen', 0.2) * (1.0 + ANALYSIS_BASELINE['rune_effect']['cooltime_reduction'])
|
||||
|
||||
# 쿨타임 감소 (왜곡 룬 -25%)
|
||||
cooltime_reduction = ANALYSIS_BASELINE['rune_effect']['cooltime_reduction']
|
||||
|
||||
# 공격 스킬만 필터링 (유틸리티 제외, 궁극기 제외)
|
||||
attack_skills = []
|
||||
for skill_id, skill in skills.items():
|
||||
if skill_id in UTILITY_SKILLS:
|
||||
continue
|
||||
if skill.get('bIsUltimate', False):
|
||||
continue
|
||||
if not skill.get('montageData'):
|
||||
continue
|
||||
|
||||
# 시퀀스 길이 계산 (Ready, Equipment 제외)
|
||||
sequence_length = sum(
|
||||
m['actualDuration']
|
||||
for m in skill['montageData']
|
||||
if 'Ready' not in m['assetName'] and 'Equipment' not in m['assetName']
|
||||
)
|
||||
|
||||
if sequence_length > 0:
|
||||
attack_skills.append({
|
||||
'id': skill_id,
|
||||
'name': skill['name'],
|
||||
'damage_rate': skill['skillDamageRate'],
|
||||
'cooltime': skill['coolTime'] * (1.0 - cooltime_reduction),
|
||||
'casting_time': skill.get('castingTime', 0),
|
||||
'sequence_length': sequence_length,
|
||||
'mana_cost': skill['manaCost'],
|
||||
'skill_type': skill.get('skillAttackType', 'Physical')
|
||||
})
|
||||
|
||||
# 쿨타임 짧은 순서로 정렬
|
||||
attack_skills.sort(key=lambda x: x['cooltime'])
|
||||
|
||||
# 로테이션 시뮬레이션
|
||||
current_time = 0.0
|
||||
current_mana = stats.get('mp', 50)
|
||||
skill_usage = {s['id']: {'count': 0, 'damage': 0, 'next_available': 0} for s in attack_skills}
|
||||
basic_attack_time = 0.0
|
||||
|
||||
# 평타 DPS (필러로 사용)
|
||||
basic_result = calculate_basic_attack_dps(stalker_id, stalker_data, base_damage)
|
||||
basic_dps = basic_result['dps']
|
||||
|
||||
# 30초 동안 스킬 사용
|
||||
time_step = 0.1 # 0.1초 단위로 시뮬레이션
|
||||
|
||||
while current_time < duration:
|
||||
# 현재 사용 가능한 스킬 찾기
|
||||
skill_used = False
|
||||
|
||||
for skill in attack_skills:
|
||||
# 쿨타임 확인
|
||||
if skill_usage[skill['id']]['next_available'] > current_time:
|
||||
continue
|
||||
|
||||
# 마나 확인
|
||||
if current_mana < skill['mana_cost']:
|
||||
continue
|
||||
|
||||
# 스킬 사용
|
||||
skill_time = skill['casting_time'] + skill['sequence_length']
|
||||
if current_time + skill_time > duration:
|
||||
break # 시간 초과
|
||||
|
||||
# 피해 계산
|
||||
if 'Magical' in skill['skill_type']:
|
||||
skill_damage = base_damage * skill['damage_rate']
|
||||
else:
|
||||
skill_damage = base_damage * skill['damage_rate']
|
||||
|
||||
skill_usage[skill['id']]['count'] += 1
|
||||
skill_usage[skill['id']]['damage'] += skill_damage
|
||||
skill_usage[skill['id']]['next_available'] = current_time + skill_time + skill['cooltime']
|
||||
|
||||
current_time += skill_time
|
||||
current_mana -= skill['mana_cost']
|
||||
skill_used = True
|
||||
break
|
||||
|
||||
if not skill_used:
|
||||
# 스킬 사용 불가 시 평타 사용
|
||||
basic_attack_time += time_step
|
||||
current_time += time_step
|
||||
|
||||
# 마나 회복
|
||||
current_mana = min(current_mana + mana_regen_rate * time_step, stats.get('mp', 50))
|
||||
|
||||
# 총 피해 계산
|
||||
total_skill_damage = sum(usage['damage'] for usage in skill_usage.values())
|
||||
basic_damage = basic_dps * basic_attack_time
|
||||
total_damage = total_skill_damage + basic_damage
|
||||
|
||||
rotation_dps = total_damage / duration
|
||||
|
||||
return {
|
||||
'dps': round(rotation_dps, 2),
|
||||
'duration': duration,
|
||||
'base_damage': round(base_damage, 2),
|
||||
'skill_damage': round(total_skill_damage, 2),
|
||||
'basic_damage': round(basic_damage, 2),
|
||||
'basic_attack_time': round(basic_attack_time, 2),
|
||||
'skill_usage': {
|
||||
skill_id: {
|
||||
'name': next((s['name'] for s in attack_skills if s['id'] == skill_id), ''),
|
||||
'count': usage['count'],
|
||||
'damage': round(usage['damage'], 2)
|
||||
}
|
||||
for skill_id, usage in skill_usage.items() if usage['count'] > 0
|
||||
},
|
||||
'notes': ''
|
||||
}
|
||||
|
||||
|
||||
def calculate_burst_dps(stalker_id: str, stalker_data: Dict, base_damage: float, duration: float = 10.0) -> Dict:
|
||||
"""시나리오 3: 버스트 DPS (10초)"""
|
||||
skills = stalker_data.get('skills', {})
|
||||
stats = stalker_data['stats']
|
||||
|
||||
# 궁극기 찾기 (유틸리티 제외)
|
||||
ultimate_skill = None
|
||||
ultimate_id = None
|
||||
for skill_id, skill in skills.items():
|
||||
if skill.get('bIsUltimate', False) and skill_id not in UTILITY_SKILLS:
|
||||
ultimate_skill = skill
|
||||
ultimate_id = skill_id
|
||||
break
|
||||
|
||||
# 모든 공격 스킬 (유틸리티 제외)
|
||||
attack_skills = []
|
||||
for skill_id, skill in skills.items():
|
||||
if skill_id in UTILITY_SKILLS:
|
||||
continue
|
||||
if skill.get('bIsUltimate', False):
|
||||
continue
|
||||
if not skill.get('montageData'):
|
||||
continue
|
||||
|
||||
# 시퀀스 길이 계산
|
||||
sequence_length = sum(
|
||||
m['actualDuration']
|
||||
for m in skill['montageData']
|
||||
if 'Ready' not in m['assetName'] and 'Equipment' not in m['assetName']
|
||||
)
|
||||
|
||||
if sequence_length > 0:
|
||||
attack_skills.append({
|
||||
'id': skill_id,
|
||||
'name': skill['name'],
|
||||
'damage_rate': skill['skillDamageRate'],
|
||||
'casting_time': skill.get('castingTime', 0),
|
||||
'sequence_length': sequence_length,
|
||||
'mana_cost': skill['manaCost']
|
||||
})
|
||||
|
||||
# 버스트 시나리오: 궁극기 → 모든 스킬 → 평타
|
||||
current_time = 0.0
|
||||
total_damage = 0.0
|
||||
skill_order = []
|
||||
|
||||
# 1. 궁극기 사용 (있는 경우)
|
||||
if ultimate_skill:
|
||||
ult_sequence = sum(
|
||||
m['actualDuration']
|
||||
for m in ultimate_skill.get('montageData', [])
|
||||
if 'Ready' not in m['assetName'] and 'Equipment' not in m['assetName']
|
||||
)
|
||||
ult_time = ultimate_skill.get('castingTime', 0) + ult_sequence
|
||||
|
||||
if current_time + ult_time <= duration:
|
||||
ult_damage = base_damage * ultimate_skill['skillDamageRate']
|
||||
total_damage += ult_damage
|
||||
current_time += ult_time
|
||||
skill_order.append({
|
||||
'time': round(current_time, 2),
|
||||
'skill': ultimate_skill['name'],
|
||||
'damage': round(ult_damage, 2),
|
||||
'type': 'ultimate'
|
||||
})
|
||||
|
||||
# 2. 모든 스킬 한 번씩 사용 (피해량 높은 순서)
|
||||
attack_skills.sort(key=lambda x: x['damage_rate'], reverse=True)
|
||||
|
||||
for skill in attack_skills:
|
||||
skill_time = skill['casting_time'] + skill['sequence_length']
|
||||
if current_time + skill_time > duration:
|
||||
continue
|
||||
|
||||
skill_damage = base_damage * skill['damage_rate']
|
||||
total_damage += skill_damage
|
||||
current_time += skill_time
|
||||
skill_order.append({
|
||||
'time': round(current_time, 2),
|
||||
'skill': skill['name'],
|
||||
'damage': round(skill_damage, 2),
|
||||
'type': 'skill'
|
||||
})
|
||||
|
||||
# 3. 남은 시간 평타
|
||||
remaining_time = duration - current_time
|
||||
basic_result = calculate_basic_attack_dps(stalker_id, stalker_data, base_damage)
|
||||
basic_dps = basic_result['dps']
|
||||
basic_damage = basic_dps * remaining_time
|
||||
total_damage += basic_damage
|
||||
|
||||
burst_dps = total_damage / duration
|
||||
|
||||
return {
|
||||
'dps': round(burst_dps, 2),
|
||||
'duration': duration,
|
||||
'base_damage': round(base_damage, 2),
|
||||
'ultimate_damage': round(skill_order[0]['damage'], 2) if skill_order and skill_order[0]['type'] == 'ultimate' else 0,
|
||||
'skill_damage': round(sum(s['damage'] for s in skill_order if s['type'] == 'skill'), 2),
|
||||
'basic_damage': round(basic_damage, 2),
|
||||
'remaining_time': round(remaining_time, 2),
|
||||
'skill_order': skill_order,
|
||||
'has_ultimate': ultimate_skill is not None,
|
||||
'notes': ''
|
||||
}
|
||||
|
||||
|
||||
def calculate_dot_dps_by_hp(target_hp_list: List[int] = [100, 500, 1000]) -> Dict:
|
||||
"""DoT 스킬 DPS (대상 HP별)"""
|
||||
dot_results = {}
|
||||
|
||||
for skill_id, skill_info in DOT_SKILLS.items():
|
||||
dot_type = skill_info['dot_type']
|
||||
dot_config = DOT_DAMAGE[dot_type]
|
||||
|
||||
dot_results[skill_id] = {
|
||||
'stalker': skill_info['stalker'],
|
||||
'name': skill_info['name'],
|
||||
'dot_type': dot_type,
|
||||
'description': dot_config['description'],
|
||||
'dps_by_hp': {}
|
||||
}
|
||||
|
||||
for target_hp in target_hp_list:
|
||||
if 'rate' in dot_config:
|
||||
# 퍼센트 피해 (Poison, Burn)
|
||||
dot_damage = target_hp * dot_config['rate']
|
||||
dot_dps = dot_damage / dot_config['duration']
|
||||
else:
|
||||
# 고정 피해 (Bleed)
|
||||
dot_damage = dot_config['damage']
|
||||
dot_dps = dot_damage / dot_config['duration']
|
||||
|
||||
dot_results[skill_id]['dps_by_hp'][target_hp] = round(dot_dps, 2)
|
||||
|
||||
return dot_results
|
||||
|
||||
|
||||
def calculate_summon_independent_dps(stalker_data: Dict, base_damage: float) -> Dict:
|
||||
"""소환체 독립 DPS 계산"""
|
||||
summon_results = {}
|
||||
|
||||
for skill_id, summon_info in SUMMON_SKILLS.items():
|
||||
stalker_id = summon_info['stalker']
|
||||
|
||||
# 해당 스토커의 스킬인지 확인
|
||||
skills = stalker_data.get('skills', {})
|
||||
if skill_id not in skills:
|
||||
continue
|
||||
|
||||
skill = skills[skill_id]
|
||||
active_duration = skill.get('activeDuration', 0)
|
||||
|
||||
if summon_info['summon'] == 'Ifrit':
|
||||
# Ifrit: 3개 몽타주 순차 루프 (2.87 + 2.90 + 2.52 = 8.29초 사이클)
|
||||
# 20초 지속
|
||||
montage_data = skill.get('montageData', [])
|
||||
cycle_time = sum(m['actualDuration'] for m in montage_data if 'Ready' not in m['assetName'])
|
||||
attack_count = (active_duration / cycle_time) * len(montage_data)
|
||||
|
||||
# Ifrit 공격: BaseDamage × 1.2
|
||||
total_damage = base_damage * 1.2 * attack_count
|
||||
summon_dps = total_damage / active_duration if active_duration > 0 else 0
|
||||
|
||||
summon_results[skill_id] = {
|
||||
'name': summon_info['name'],
|
||||
'summon': summon_info['summon'],
|
||||
'active_duration': active_duration,
|
||||
'cycle_time': round(cycle_time, 2),
|
||||
'attack_count': round(attack_count, 2),
|
||||
'dps': round(summon_dps, 2),
|
||||
'notes': f'{len(montage_data)}개 몽타주 순차 루프'
|
||||
}
|
||||
|
||||
elif summon_info['summon'] == 'Shiva':
|
||||
# Shiva: 단일 몽타주 2.32초 반복
|
||||
# 60초 지속
|
||||
montage_name = summon_info.get('montage', '')
|
||||
# TODO: 실제 몽타주 시간 찾아서 계산
|
||||
cycle_time = 2.32 # 임시값
|
||||
attack_count = active_duration / cycle_time
|
||||
|
||||
# Shiva 공격: BaseDamage × 0.8
|
||||
total_damage = base_damage * 0.8 * attack_count
|
||||
summon_dps = total_damage / active_duration if active_duration > 0 else 0
|
||||
|
||||
summon_results[skill_id] = {
|
||||
'name': summon_info['name'],
|
||||
'summon': summon_info['summon'],
|
||||
'active_duration': active_duration,
|
||||
'cycle_time': round(cycle_time, 2),
|
||||
'attack_count': round(attack_count, 2),
|
||||
'dps': round(summon_dps, 2),
|
||||
'notes': '단일 몽타주 반복'
|
||||
}
|
||||
|
||||
return summon_results
|
||||
|
||||
|
||||
def save_dps_results_json(all_results: Dict, output_dir: Path) -> None:
|
||||
"""DPS 계산 결과를 JSON으로 저장 (Claude 분석용)"""
|
||||
|
||||
# 정렬된 데이터 준비
|
||||
scenario1_sorted = sorted(
|
||||
[(sid, all_results[sid]['scenario1']) for sid in STALKERS if sid in all_results],
|
||||
key=lambda x: x[1]['dps'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
scenario2_sorted = sorted(
|
||||
[(sid, all_results[sid]['scenario2']) for sid in STALKERS if sid in all_results],
|
||||
key=lambda x: x[1]['dps'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
scenario3_sorted = sorted(
|
||||
[(sid, all_results[sid]['scenario3']) for sid in STALKERS if sid in all_results],
|
||||
key=lambda x: x[1]['dps'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
# 정렬된 순위 정보 추가
|
||||
for rank, (stalker_id, _) in enumerate(scenario1_sorted, 1):
|
||||
all_results[stalker_id]['scenario1']['rank'] = rank
|
||||
|
||||
for rank, (stalker_id, _) in enumerate(scenario2_sorted, 1):
|
||||
all_results[stalker_id]['scenario2']['rank'] = rank
|
||||
|
||||
for rank, (stalker_id, _) in enumerate(scenario3_sorted, 1):
|
||||
all_results[stalker_id]['scenario3']['rank'] = rank
|
||||
|
||||
# JSON 저장
|
||||
output_file = output_dir / "dps_raw_results.json"
|
||||
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(all_results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Generated: {output_file}")
|
||||
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
print("=" * 80)
|
||||
print("DPS 시나리오 계산 v2")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. 출력 디렉토리 가져오기 (가장 최근 v2 폴더)
|
||||
output_dir = get_output_dir(create_new=False)
|
||||
print(f"\nOutput directory: {output_dir}")
|
||||
|
||||
# 2. validated_data.json 로드
|
||||
print("\nLoading validated_data.json...")
|
||||
validated_data = load_validated_data(output_dir)
|
||||
print(f"Loaded data for {len(validated_data)} stalkers")
|
||||
|
||||
# 3. 각 스토커별 DPS 계산
|
||||
print("\nCalculating DPS scenarios...")
|
||||
all_results = {}
|
||||
|
||||
for stalker_id in STALKERS:
|
||||
print(f"\n Processing: {STALKER_INFO[stalker_id]['name']} ({stalker_id})...")
|
||||
|
||||
stalker_data = validated_data.get(stalker_id, {})
|
||||
if not stalker_data:
|
||||
print(f" WARNING: No data for {stalker_id}, skipping")
|
||||
continue
|
||||
|
||||
stats = stalker_data['stats']['stats']
|
||||
|
||||
# BaseDamage 계산
|
||||
base_damage = calculate_base_damage(stalker_id, stats)
|
||||
print(f" BaseDamage: {round(base_damage, 2)}")
|
||||
|
||||
# 시나리오 1: 평타 DPS
|
||||
scenario1 = calculate_basic_attack_dps(stalker_id, stalker_data, base_damage)
|
||||
print(f" Basic Attack DPS: {scenario1['dps']}")
|
||||
|
||||
# 시나리오 2: 스킬 로테이션 DPS (30초)
|
||||
scenario2 = calculate_skill_rotation_dps(stalker_id, stalker_data, base_damage, 30.0)
|
||||
print(f" Rotation DPS: {scenario2['dps']}")
|
||||
|
||||
# 시나리오 3: 버스트 DPS (10초)
|
||||
scenario3 = calculate_burst_dps(stalker_id, stalker_data, base_damage, 10.0)
|
||||
print(f" Burst DPS: {scenario3['dps']}")
|
||||
|
||||
all_results[stalker_id] = {
|
||||
'scenario1': scenario1,
|
||||
'scenario2': scenario2,
|
||||
'scenario3': scenario3
|
||||
}
|
||||
|
||||
# 소환체 분석 (Rene만)
|
||||
if stalker_id == 'rene':
|
||||
summon_analysis = calculate_summon_independent_dps(stalker_data, base_damage)
|
||||
all_results[stalker_id]['summon_analysis'] = summon_analysis
|
||||
print(f" Summon analysis complete: {len(summon_analysis)} skills")
|
||||
|
||||
# 4. DoT 분석 (전역)
|
||||
print("\n Calculating DoT DPS...")
|
||||
dot_analysis = calculate_dot_dps_by_hp([100, 500, 1000])
|
||||
all_results['dot_analysis'] = dot_analysis
|
||||
print(f" DoT analysis complete: {len(dot_analysis)} skills")
|
||||
|
||||
# 5. JSON 저장 (Claude 분석용)
|
||||
print("\nSaving DPS results to JSON...")
|
||||
save_dps_results_json(all_results, output_dir)
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("DPS calculation complete!")
|
||||
print("=" * 80)
|
||||
print("\nNext step: Run Claude analysis to generate 02_DPS_시나리오_비교분석_v2.md")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -136,6 +136,9 @@ UTILITY_SKILLS = {
|
||||
'SK130101': 'baran - 무기 막기',
|
||||
'SK150206': 'clad - 치유',
|
||||
'SK150202': 'clad - 신성한 빛 (DOT 제거)',
|
||||
'SK150301': 'clad - 마석 황금 (보호막)', # 궁극기 - 보호막 스킬
|
||||
'SK160301': 'rene - 마석 붉은 축제 (흡혈 버프)', # 궁극기 - 흡혈 버프
|
||||
'SK190301': 'lian - 마석 폭우 (쿨타임 감소)', # 궁극기 - 쿨타임 감소 버프
|
||||
'SK180205': 'sinobu - 바꿔치기 (피격 시 효과)',
|
||||
'SK180206': 'sinobu - 인술 칠흑안개',
|
||||
'SK190209': 'lian - 재장전', # 재장전
|
||||
@ -175,7 +178,40 @@ BASE_DAMAGE_FORMULA = {
|
||||
'physical_str': lambda stats: (stats['str'] + 80) * 1.20,
|
||||
'physical_dex': lambda stats: (stats['dex'] + 80) * 1.20,
|
||||
'magical': lambda stats: (stats['int'] + 80) * 1.10,
|
||||
'support': lambda stats: (stats.get('wis', stats.get('con', 0)) + 80) * 1.00
|
||||
'support': lambda stats: (stats['str'] + 80) * 1.00 # Clad uses STR, not WIS
|
||||
}
|
||||
|
||||
# 콤보 캔슬 시스템 (v2.1)
|
||||
# ANS_DisableBlockingState_C 노티파이로 조기 캔슬 가능
|
||||
COMBO_CANCEL_STALKERS = {
|
||||
'hilda': {
|
||||
'weapons': ['weaponShield'], # 방패+무기
|
||||
'patterns': ['AM_PC_Hilda_B_Attack_W01_'], # 평타 패턴
|
||||
'time_reduction': 0.19, # 19% 시간 단축
|
||||
'description': '3타 콤보 캔슬 (4.57s → 3.69s)'
|
||||
},
|
||||
'baran': {
|
||||
'weapons': ['twoHandWeapon'], # 양손 무기
|
||||
'patterns': ['AM_PC_Baran_B_Attack_W01_'],
|
||||
'time_reduction': 0.19, # 19% 시간 단축
|
||||
'description': '평타 콤보 캔슬 (5.53s → 4.48s)'
|
||||
},
|
||||
'clad': {
|
||||
'weapons': ['oneHandWeapon'], # 한손 무기 (mace)
|
||||
'patterns': ['AM_PC_Clad_Base_Attack_Mace'],
|
||||
'time_reduction': 0.56, # 56% 시간 단축 (극적!)
|
||||
'description': '평타 콤보 캔슬 (4.17s → 1.84s)'
|
||||
}
|
||||
}
|
||||
|
||||
# 특수 궁극기 처리 (v2.1)
|
||||
SPECIAL_ULTIMATE_HANDLING = {
|
||||
'SK130301': { # 바란 - 일격분쇄
|
||||
'stalker': 'baran',
|
||||
'use_an_simplesendevent_time': True, # AN_SimpleSendEvent 시간 사용
|
||||
'event_tag': 'Ability.Attack.Ready',
|
||||
'description': 'AN_SimpleSendEvent 시점(1.29초)이 실제 발동 시간, 10초는 최대 홀딩 시간'
|
||||
}
|
||||
}
|
||||
|
||||
# 검증 기준
|
||||
|
||||
@ -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. 결과 저장 (새 디렉토리 생성)
|
||||
|
||||
@ -18,25 +18,31 @@ import config
|
||||
|
||||
def generate_header() -> str:
|
||||
"""문서 헤더 생성"""
|
||||
return f"""# 03. 스토커별 기본 데이터 (v2)
|
||||
return f"""# 01. 분석 기초자료 (v2)
|
||||
|
||||
## 📌 문서 개요
|
||||
|
||||
본 문서는 던전 스토커즈 전투 시스템의 **기초 데이터**를 종합 정리한 자료입니다.
|
||||
|
||||
### 구성
|
||||
1. **분석 전제조건**: 레벨, 기어스코어, 룬 빌드, 장비 스탯 추정
|
||||
2. **스토커별 기본 데이터**: 10명 스토커의 스탯, 스킬, 평타 정보
|
||||
3. **특수 시스템 상세**: Parrying, Chain Score, Reload, Charging 등
|
||||
|
||||
## 데이터 소스
|
||||
- `DT_CharacterStat`: 기본 스탯, 스킬 목록
|
||||
- `DT_CharacterAbility`: 평타 몽타주
|
||||
- `DT_Skill`: 스킬 상세 정보 (이름, 피해배율, 쿨타임, 마나, 시전시간, 효과)
|
||||
- `DT_Rune`, `DT_RuneGroup`: 룬 시스템 데이터
|
||||
- `DT_Equip`, `DT_Float`: 장비 및 기어스코어 상수
|
||||
- `Blueprint`: 스킬 변수 (ActivationOrderGroup 등)
|
||||
- `AnimMontage`: 타이밍 및 공격 노티파이, 실제 발사 시점
|
||||
|
||||
## 검증 상태
|
||||
- ✅ 모든 데이터는 최신 JSON (2025-10-24 15:58:55)에서 추출
|
||||
- ✅ 모든 데이터는 최신 JSON에서 추출
|
||||
- ✅ 교차 검증 완료
|
||||
- ✅ 출처 명시 (각 데이터 필드별)
|
||||
|
||||
## DPS 계산 시 고려사항
|
||||
- **시전시간**: 스킬 사용 시 시전시간(CastingTime)이 추가됨
|
||||
- **실제 공격 시점**: 원거리 스킬(우르드, 리안)의 경우 몽타주 시간보다 빠르게 공격 가능
|
||||
- **DoT 데미지**: DoT(Damage over Time) 스킬은 대상 HP에 비례하여 지속 피해 발생 (구체적 계산은 다음 챕터 참조)
|
||||
|
||||
---
|
||||
|
||||
"""
|
||||
@ -511,6 +517,223 @@ def generate_skill_entry(skill: Dict, index: int, is_sub: bool = False, is_ultim
|
||||
|
||||
return md
|
||||
|
||||
def generate_analysis_prerequisites(data: Dict) -> str:
|
||||
"""분석 전제조건 섹션 생성"""
|
||||
md = "## 📋 분석 전제조건\n\n"
|
||||
|
||||
# 공통 설정
|
||||
md += "### 기본 설정\n"
|
||||
md += f"- **레벨**: {config.ANALYSIS_BASELINE['level']}\n"
|
||||
md += f"- **기어 스코어**: {config.ANALYSIS_BASELINE['gear_score']}\n"
|
||||
md += f"- **플레이 스타일**: {config.ANALYSIS_BASELINE['play_style']}\n\n"
|
||||
|
||||
# 장비 스탯 추정 (metadata에서 추출)
|
||||
md += "### 장비 스탯 추정 (기어스코어 400 기준)\n\n"
|
||||
metadata = data.get('_metadata', {})
|
||||
gear_constants = metadata.get('gearScoreConstants', {})
|
||||
|
||||
md += "**무기** (레벨 20, Rare 등급 기준):\n"
|
||||
md += "- PhysicalDamage: +65\n"
|
||||
md += "- MagicalDamage: +65\n\n"
|
||||
|
||||
md += "**방어구 3부위** (갑옷, 다리, 액세서리):\n"
|
||||
md += "- 총 PhysicalDamage: +15\n"
|
||||
md += "- 총 MagicalDamage: +15\n"
|
||||
md += "- HP: +120\n"
|
||||
md += "- Defense: +80\n\n"
|
||||
|
||||
md += "**총 장비 보너스**:\n"
|
||||
md += "- PhysicalDamage: +80\n"
|
||||
md += "- MagicalDamage: +80\n"
|
||||
md += "- HP: +120\n"
|
||||
md += "- Defense: +80\n\n"
|
||||
|
||||
# 룬 빌드 설정 (metadata에서 룬 데이터 활용)
|
||||
md += "### 역할별 최적 룬 빌드\n\n"
|
||||
runes = metadata.get('runes', {})
|
||||
rune_groups = metadata.get('runeGroups', {})
|
||||
|
||||
# 물리 딜러 빌드 예시 (룬 데이터에서 추출)
|
||||
md += "#### 물리 딜러 (Hilda, Baran, Rio, Sinobu, Cazimord)\n\n"
|
||||
md += "**Main: 스킬 그룹 (20xxx)**\n"
|
||||
md += "- 20101 저주 (조건부 지연 피해)\n"
|
||||
md += "- 20201 파괴 (+10% 스킬 피해)\n"
|
||||
md += "- 20301 명상 (+70% 마나 회복)\n\n"
|
||||
md += "**Sub: 전투 그룹 (10xxx)**\n"
|
||||
md += "- 10201 분노 (+10% 물리 피해)\n"
|
||||
md += "- 10103 공략 (+20% 머리 공격 피해)\n\n"
|
||||
|
||||
md += "#### 마법 딜러 (Nave, Rene)\n\n"
|
||||
md += "**Main: 스킬 그룹 (20xxx)**\n"
|
||||
md += "- 20103 활기 (마나 높을 때 스킬 피해 증가)\n"
|
||||
md += "- 20202 왜곡 (-25% 쿨타임)\n"
|
||||
md += "- 20301 명상 (+70% 마나 회복)\n\n"
|
||||
md += "**Sub: 전투 그룹 (10xxx)**\n"
|
||||
md += "- 10301 폭풍 (+10% 마법 피해)\n"
|
||||
md += "- 10103 공략 (+20% 머리 공격 피해)\n\n"
|
||||
|
||||
md += "#### 원거리 딜러 (Urud, Lian)\n\n"
|
||||
md += "**Main: 스킬 그룹 (20xxx)**\n"
|
||||
md += "- 20101 저주 (지연 피해)\n"
|
||||
md += "- 20201 파괴 (+10% 스킬 피해)\n"
|
||||
md += "- 20301 명상 (+70% 마나 회복)\n\n"
|
||||
md += "**Sub: 전투 그룹 (10xxx)**\n"
|
||||
md += "- 10201 분노 (+10% 물리 피해)\n"
|
||||
md += "- 10103 공략 (+20% 머리 공격 피해)\n\n"
|
||||
|
||||
md += "#### 서포터 (Clad)\n\n"
|
||||
md += "**Main: 전투 그룹 (10xxx)**\n"
|
||||
md += "- 10101 충전 (+30% 궁극기 회복)\n"
|
||||
md += "- 10202 방패 (+7% 물리 저항)\n"
|
||||
md += "- 10302 수호 (+7% 마법 저항)\n\n"
|
||||
md += "**Sub: 보조 그룹 (40xxx)**\n"
|
||||
md += "- 40201 면역 (물약 사용 시 +20% 저항 20초)\n"
|
||||
md += "- 40301 효율 (+50% 물약 효과)\n\n"
|
||||
|
||||
# 특수 시스템 활용률
|
||||
md += "### 특수 시스템 활용률\n\n"
|
||||
md += "**전제**: 최적 플레이 = 100% 활용\n\n"
|
||||
|
||||
md += "#### Cazimord - Parrying (흘리기)\n"
|
||||
md += "- **판정 윈도우**: 0.2초\n"
|
||||
md += "- **성공 시 효과**:\n"
|
||||
md += " - 적 피해 무효화\n"
|
||||
md += " - 자동 반격 (높은 피해)\n"
|
||||
md += " - **스킬 쿨타임 감소**:\n"
|
||||
md += " - 섬광(SK170201): -3.8초\n"
|
||||
md += " - 날개베기(SK170202): -3.8초\n"
|
||||
md += " - 작열(SK170203): -6.8초\n"
|
||||
md += "- **활용률 시나리오**: 0% (미사용) vs 100% (완벽 성공)\n\n"
|
||||
|
||||
md += "#### Rio - Chain Score\n"
|
||||
md += "- **최대 스택**: 3\n"
|
||||
md += "- **효과**: 각 스킬별로 다른 위력 증가\n"
|
||||
md += "- **충전**: Dropping Attack 성공 시\n"
|
||||
md += "- **활용률**: 100% (항상 3스택 유지)\n\n"
|
||||
|
||||
md += "#### Urud & Lian - Reload\n"
|
||||
md += "- **탄약**: 6발\n"
|
||||
md += "- **재장전 시간**: 2.0초\n"
|
||||
md += "- **활용률**: 100% (탄약 관리 최적화)\n\n"
|
||||
|
||||
md += "#### Lian - Charging Bow\n"
|
||||
md += "- **만충전 데미지**: 1.5배\n"
|
||||
md += "- **충전 시간**: 레벨당 0.5초 (최대 1.5초)\n"
|
||||
md += "- **활용률**: 100% (항상 만충전 후 발사)\n\n"
|
||||
|
||||
md += "#### Rene - Spirit 소환\n"
|
||||
md += "- **소환수**: Ifrit, Shiva\n"
|
||||
md += "- **활용률**: 100% (소환수 항상 활용)\n\n"
|
||||
|
||||
md += "#### Sinobu - Shuriken 충전\n"
|
||||
md += "- **최대 충전**: 3개\n"
|
||||
md += "- **충전 속도**: 1초/개\n"
|
||||
md += "- **활용률**: 100% (충전 관리 최적화)\n\n"
|
||||
|
||||
md += "---\n\n"
|
||||
|
||||
return md
|
||||
|
||||
def generate_special_systems(data: Dict) -> str:
|
||||
"""특수 시스템 상세 분석 섹션 생성"""
|
||||
md = "## 🔧 특수 시스템 상세\n\n"
|
||||
|
||||
md += "### Cazimord - Parrying (흘리기)\n\n"
|
||||
md += "#### 메커니즘\n"
|
||||
md += "- **판정 윈도우**: 0.2초\n"
|
||||
md += "- **패링 성공 시**:\n"
|
||||
md += " - 적 공격 무효화\n"
|
||||
md += " - 자동 반격 (높은 피해)\n"
|
||||
md += " - 스킬 쿨타임 감소\n\n"
|
||||
|
||||
md += "#### 쿨타임 감소 효과\n"
|
||||
md += "| 스킬 | 기본 쿨타임 | 패링 성공 시 감소 | 패링 100% 시 유효 쿨타임 |\n"
|
||||
md += "|------|-------------|-------------------|------------------------|\n"
|
||||
|
||||
# Cazimord 스킬 데이터에서 쿨타임 정보 추출
|
||||
if 'cazimord' in data:
|
||||
cazimord = data['cazimord']
|
||||
skills = cazimord.get('skills', {})
|
||||
|
||||
parrying_skills = {
|
||||
'SK170201': ('섬광', -3.8),
|
||||
'SK170202': ('날개베기', -3.8),
|
||||
'SK170203': ('작열', -6.8)
|
||||
}
|
||||
|
||||
for skill_id, (skill_name, reduction) in parrying_skills.items():
|
||||
if skill_id in skills:
|
||||
skill = skills[skill_id]
|
||||
base_cooltime = skill.get('coolTime', 0)
|
||||
effective_cooltime = max(0, base_cooltime + reduction)
|
||||
md += f"| {skill_name} | {base_cooltime:.1f}초 | {reduction}초 | {effective_cooltime:.1f}초 |\n"
|
||||
|
||||
md += "\n#### DPS 영향\n"
|
||||
md += "- **패링 0%**: 기본 쿨타임 적용\n"
|
||||
md += "- **패링 100%**: 쿨타임 감소로 스킬 회전율 증가 → DPS 상승\n\n"
|
||||
|
||||
md += "### Rio - Chain Score\n\n"
|
||||
md += "#### 메커니즘\n"
|
||||
md += "- **스택 시스템**: 최대 3스택\n"
|
||||
md += "- **스택 획득**: Dropping Attack 스킬 성공 시 +1\n"
|
||||
md += "- **효과**: 스킬별로 스택 소모 및 추가 효과 발동\n\n"
|
||||
|
||||
md += "#### 스택별 효과\n"
|
||||
md += "- 각 스킬이 Chain Score 스택을 소모하여 강화\n"
|
||||
md += "- 스킬마다 다른 위력 증가 배율 적용\n\n"
|
||||
|
||||
md += "### Urud & Lian - Reload 시스템\n\n"
|
||||
md += "#### 메커니즘\n"
|
||||
md += "- **최대 탄약**: 6발\n"
|
||||
md += "- **재장전 시간**: 2.0초\n"
|
||||
md += "- **재장전 중**: 다른 행동 불가 (DPS 손실)\n\n"
|
||||
|
||||
md += "#### DPS 영향\n"
|
||||
md += "- 6발 소진 후 2초 공백 발생\n"
|
||||
md += "- 최적 플레이: 탄약 관리로 전투 공백 최소화\n\n"
|
||||
|
||||
md += "### Lian - Charging Bow\n\n"
|
||||
md += "#### 메커니즘\n"
|
||||
md += "- **충전 단계**: 3단계 (0.5초씩)\n"
|
||||
md += "- **만충전 배율**: 1.5배\n"
|
||||
md += "- **충전 중**: 이동 속도 감소\n\n"
|
||||
|
||||
md += "#### DPS 영향\n"
|
||||
md += "- 만충전 시 피해량 증가\n"
|
||||
md += "- 충전 시간 vs 피해량 트레이드오프\n\n"
|
||||
|
||||
md += "### Rene - 소환수 시스템\n\n"
|
||||
md += "#### Ifrit (화염 정령)\n"
|
||||
if 'rene' in data:
|
||||
rene = data['rene']
|
||||
summons = rene.get('summons', {})
|
||||
if 'Ifrit' in summons:
|
||||
ifrit = summons['Ifrit']
|
||||
md += f"- **지속 시간**: {ifrit.get('activeDuration', 0)}초\n"
|
||||
md += f"- **공격 타입**: 근접 화염 공격\n"
|
||||
md += f"- **독립 DPS**: 계산 필요 (소환수 몽타주 기반)\n\n"
|
||||
|
||||
md += "#### Shiva (냉기 정령)\n"
|
||||
if 'rene' in data and 'Shiva' in rene.get('summons', {}):
|
||||
shiva = summons.get('Shiva', {})
|
||||
md += f"- **지속 시간**: {shiva.get('activeDuration', 0)}초\n"
|
||||
md += f"- **공격 타입**: 원거리 냉기 공격\n"
|
||||
md += f"- **독립 DPS**: 계산 필요 (소환수 몽타주 기반)\n\n"
|
||||
|
||||
md += "### Sinobu - Shuriken 충전\n\n"
|
||||
md += "#### 메커니즘\n"
|
||||
md += "- **최대 충전**: 3개\n"
|
||||
md += "- **충전 속도**: 1초/개 (자동)\n"
|
||||
md += "- **소모**: 특정 스킬 사용 시 1개씩 소모\n\n"
|
||||
|
||||
md += "#### DPS 영향\n"
|
||||
md += "- 충전 관리로 스킬 사용 빈도 조절\n"
|
||||
md += "- 최적 플레이: 충전 타이밍 고려한 스킬 로테이션\n\n"
|
||||
|
||||
md += "---\n\n"
|
||||
|
||||
return md
|
||||
|
||||
def main():
|
||||
"""메인 실행 함수"""
|
||||
print("="*80)
|
||||
@ -527,7 +750,7 @@ def main():
|
||||
elif intermediate_file.exists():
|
||||
data_file = intermediate_file
|
||||
print(f"\n[ 중간 데이터 사용 ]: {data_file}")
|
||||
print("⚠️ 검증되지 않은 데이터입니다. validate_stalker_data.py를 먼저 실행하는 것을 권장합니다.")
|
||||
print("[WARN] 검증되지 않은 데이터입니다. validate_stalker_data.py를 먼저 실행하는 것을 권장합니다.")
|
||||
else:
|
||||
print(f"[FAIL] 데이터 파일 없음")
|
||||
print("먼저 extract_stalker_data_v2.py를 실행하세요.")
|
||||
@ -540,11 +763,13 @@ def main():
|
||||
|
||||
# 마크다운 생성
|
||||
md_content = generate_header()
|
||||
md_content += generate_analysis_prerequisites(data) # 분석 전제조건 추가
|
||||
md_content += generate_stalker_overview(data)
|
||||
md_content += generate_ultimate_overview(data)
|
||||
md_content += generate_dot_overview(data) # DoT 스킬 종합
|
||||
|
||||
# 개별 스토커
|
||||
stalker_count = 0
|
||||
for stalker_id in config.STALKERS:
|
||||
if stalker_id not in data:
|
||||
print(f"[WARN] {stalker_id}: 데이터 없음, 건너뜀")
|
||||
@ -552,20 +777,26 @@ def main():
|
||||
|
||||
print(f" - {stalker_id} 문서 생성 중...")
|
||||
md_content += generate_stalker_detail(stalker_id, data[stalker_id])
|
||||
stalker_count += 1
|
||||
|
||||
# 특수 시스템 상세 추가
|
||||
md_content += generate_special_systems(data)
|
||||
|
||||
# Footer
|
||||
md_content += "---\n\n"
|
||||
md_content += f"**생성 일시**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
md_content += f"**데이터 소스**: {data_file.name}\n"
|
||||
md_content += f"**검증 상태**: {'검증 완료 ✅' if data_file.name == 'validated_data.json' else '미검증 ⚠️'}\n"
|
||||
md_content += f"**검증 상태**: {'검증 완료' if data_file.name == 'validated_data.json' else '미검증'}\n"
|
||||
|
||||
# 파일 저장
|
||||
output_file = config.OUTPUT_DIR / "03_스토커별_기본데이터_v2.md"
|
||||
# 파일 저장 - 새 파일명 사용
|
||||
output_file = config.OUTPUT_DIR / "01_분석_기초자료_v2.md"
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(md_content)
|
||||
|
||||
print(f"\n[OK] 문서 생성 완료: {output_file}")
|
||||
print(f" - 총 {len(data)}명 스토커 문서 생성")
|
||||
print(f" - 총 {stalker_count}명 스토커 문서 생성")
|
||||
print(f" - 분석 전제조건 포함")
|
||||
print(f" - 특수 시스템 상세 포함")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
121
분석도구/v2/output.txt
Normal file
121
분석도구/v2/output.txt
Normal file
@ -0,0 +1,121 @@
|
||||
================================================================================
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> v2
|
||||
================================================================================
|
||||
|
||||
[ JSON <20><><EFBFBD><EFBFBD> <20>ε<EFBFBD> ]
|
||||
Loading: DataTable.json
|
||||
Loading: Blueprint.json
|
||||
Loading: AnimMontage.json
|
||||
|
||||
=== DT_CharacterStat <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] Hilda (<28><><EFBFBD><EFBFBD>) (hilda)
|
||||
[OK] Urud (<28>츣<EFBFBD><ECB8A3>) (urud)
|
||||
[OK] Nave (<28><><EFBFBD>̺<EFBFBD>) (nave)
|
||||
[OK] Baran (<28>ٶ<EFBFBD>) (baran)
|
||||
[OK] Rio (<28><><EFBFBD><EFBFBD>) (rio)
|
||||
[OK] Clad (Ŭ<><C5AC><EFBFBD><EFBFBD>) (clad)
|
||||
[OK] Rene (<28><><EFBFBD><EFBFBD>) (rene)
|
||||
[OK] Sinobu (<28>ó<EFBFBD><C3B3><EFBFBD>) (sinobu)
|
||||
[OK] Lian (<28><><EFBFBD><EFBFBD>) (lian)
|
||||
[OK] Cazimord (ī<><C4AB><EFBFBD><EFBFBD>) (cazimord)
|
||||
|
||||
=== DT_CharacterAbility <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] hilda: {'weaponShield': 3}
|
||||
[OK] urud: {'bow': 1}
|
||||
[OK] nave: {'staff': 2}
|
||||
[OK] baran: {'twoHandWeapon': 3}
|
||||
[OK] rio: {'shortSword': 3}
|
||||
[OK] clad: {'mace': 2}
|
||||
[OK] rene: {'staff': 3}
|
||||
[OK] sinobu: {'shortSword': 2}
|
||||
[OK] lian: {'bow': 1}
|
||||
[OK] cazimord: {'weaponShield': 3}
|
||||
|
||||
=== DT_Skill <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] <20><> 91<39><31> <20><>ų <20><><EFBFBD><EFBFBD>
|
||||
- hilda: 8<><38>
|
||||
- urud: 10<31><30>
|
||||
- nave: 9<><39>
|
||||
- baran: 8<><38>
|
||||
- rio: 8<><38>
|
||||
- clad: 8<><38>
|
||||
- rene: 8<><38>
|
||||
- sinobu: 8<><38>
|
||||
- lian: 10<31><30>
|
||||
- cazimord: 14<31><34>
|
||||
|
||||
=== GA_Skill Blueprint <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] <20><> 112<31><32> GA_Skill Blueprint <20><><EFBFBD><EFBFBD>
|
||||
|
||||
=== AnimMontage <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] <20><> 743<34><33> <20><>Ÿ<EFBFBD><C5B8> <20><><EFBFBD><EFBFBD> (PC + Summon)
|
||||
[INFO] <20><EFBFBD> ĵ<><C4B5> <20><><EFBFBD><EFBFBD> <20><>Ÿ<EFBFBD><C5B8>: 9<><39>
|
||||
- AM_PC_Baran_B_Attack_W01_01: ĵ<><C4B5> 1.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.90<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Baran_B_Attack_W01_02: ĵ<><C4B5> 1.48<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.93<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Baran_B_Attack_W01_03: ĵ<><C4B5> 1.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.73<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Clad_B_Attack_W01_03: ĵ<><C4B5> 1.30<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.73<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Clad_B_Attack_W01_02: ĵ<><C4B5> 0.97<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 2.27<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Clad_B_Attack_W01_01: ĵ<><C4B5> 1.14<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 2.00<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Hilda_B_Attack_W01_01: ĵ<><C4B5> 1.23<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.60<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Hilda_B_Attack_W01_02: ĵ<><C4B5> 1.23<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.60<EFBFBD><EFBFBD>)
|
||||
- AM_PC_Hilda_B_Attack_W01_03: ĵ<><C4B5> 1.23<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD> 1.37<EFBFBD><EFBFBD>)
|
||||
[INFO] <20><>ȯ<EFBFBD><C8AF> <20><><EFBFBD><EFBFBD> <20><>Ÿ<EFBFBD><C5B8>: 15<31><35>
|
||||
- AM_PC_Rene_B_Skill_SummonIfrit: 2.33<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.46<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
|
||||
- AM_PC_Rene_B_Skill_SummonShiva: 3.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.69<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
|
||||
- AM_Sum_Elemental_Fire_Attack: 1.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Fire_Attack_Splash: 1.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Fire_Death: 2.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Fire_Shock: 2.87<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.87<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Fire_Attack_N01: 3.67<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.29<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
|
||||
- AM_Sum_Elemental_Fire_Attack_N02: 3.67<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.29<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=True
|
||||
- AM_Sum_Elemental_Fire_Stun: 3.50<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.50<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Fire_Appear: 1.70<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 1.70<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Fire_Attack_N03: 3.33<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.70<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Ice_Death: 9.33<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 9.33<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Ice_Attack_N01: 4.63<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 2.32<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Ice_Appear: 3.40<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.40<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
- AM_Sum_Elemental_Ice_Attack: 3.00<EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>: 3.00<EFBFBD><EFBFBD>), <20><><EFBFBD><EFBFBD>=False
|
||||
|
||||
=== DT_NPCAbility <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] Ifrit (none): 2<><32> <20><>Ÿ<EFBFBD><C5B8>
|
||||
1. AM_Sum_Elemental_Fire_Attack_N01
|
||||
2. AM_Sum_Elemental_Fire_Attack_N02
|
||||
[OK] Ifrit (normal): 1<><31> <20><>Ÿ<EFBFBD><C5B8>
|
||||
1. AM_Sum_Elemental_Fire_Attack_N03
|
||||
|
||||
=== DT_Rune <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] 190<39><30> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>
|
||||
|
||||
=== DT_RuneGroup <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] <20><><EFBFBD><EFBFBD> <20><EFBFBD>: Core(3), Sub1(2), Sub2(2)
|
||||
[OK] <20><>ų <20><EFBFBD>: Core(3), Sub1(3), Sub2(2)
|
||||
[OK] <20><><EFBFBD><EFBFBD> <20><EFBFBD>: Core(3), Sub1(2), Sub2(3)
|
||||
[OK] <20><><EFBFBD><EFBFBD> <20><EFBFBD>: Core(2), Sub1(2), Sub2(2)
|
||||
[OK] <20><><EFBFBD><EFBFBD> <20><EFBFBD>: Core(3), Sub1(3), Sub2(3)
|
||||
|
||||
=== DT_Equip <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] 485<38><35> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>
|
||||
|
||||
=== DT_Float (<28><><EFBFBD><EFBFBD>ھ<EFBFBD> <20><><EFBFBD><EFBFBD>) <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] GearScoreEquipCommon: 10
|
||||
[OK] GearScoreEquipUncommon: 30
|
||||
[OK] GearScoreEquipRare: 50
|
||||
[OK] GearScoreEquipLegendary: 100
|
||||
[OK] GearScoreSkillPassive: 50
|
||||
[OK] GearScoreSkillPerk: 50
|
||||
|
||||
=== <20><><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> ===
|
||||
[OK] Hilda (<28><><EFBFBD><EFBFBD>) (hilda): 5<><35> <20><>ų
|
||||
[OK] Urud (<28>츣<EFBFBD><ECB8A3>) (urud): 6<><36> <20><>ų
|
||||
[OK] Nave (<28><><EFBFBD>̺<EFBFBD>) (nave): 5<><35> <20><>ų
|
||||
[OK] Baran (<28>ٶ<EFBFBD>) (baran): 5<><35> <20><>ų
|
||||
[OK] Rio (<28><><EFBFBD><EFBFBD>) (rio): 5<><35> <20><>ų
|
||||
[OK] Clad (Ŭ<><C5AC><EFBFBD><EFBFBD>) (clad): 5<><35> <20><>ų
|
||||
[OK] Rene (<28><><EFBFBD><EFBFBD>) (rene): 5<><35> <20><>ų
|
||||
[OK] Sinobu (<28>ó<EFBFBD><C3B3><EFBFBD>) (sinobu): 5<><35> <20><>ų
|
||||
[OK] Lian (<28><><EFBFBD><EFBFBD>) (lian): 6<><36> <20><>ų
|
||||
[OK] Cazimord (ī<><C4AB><EFBFBD><EFBFBD>) (cazimord): 5<><35> <20><>ų
|
||||
|
||||
[OK] <20>߰<EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20>Ϸ<EFBFBD>: D:\Work\WorldStalker\DS-<2D><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\<5C>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD>\20251028_031316_v2\intermediate_data.json
|
||||
- <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>丮: D:\Work\WorldStalker\DS-<2D><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD>_<EFBFBD><5F><EFBFBD><EFBFBD><EFBFBD><EFBFBD>\<5C>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD>\20251028_031316_v2
|
||||
- <20><> 11<31><31> <20><><EFBFBD><EFBFBD>Ŀ <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
Reference in New Issue
Block a user