feat: 허수아비 계산 시뮬레이터 추가
- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가 - 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가 - DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
This commit is contained in:
338
Assets/_Game/Scripts/Combat/Simulation/BuildSimulationInput.cs
Normal file
338
Assets/_Game/Scripts/Combat/Simulation/BuildSimulationInput.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
using Colosseum.Passives;
|
||||
using Colosseum.Skills;
|
||||
using Colosseum.Weapons;
|
||||
|
||||
namespace Colosseum.Combat.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// 허수아비 계산 시뮬레이터가 사용할 완성형 빌드 입력값입니다.
|
||||
/// 프리셋 없이 직접 구성한 빌드와 기존 프리셋 기반 입력을 모두 지원합니다.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class BuildSimulationInput
|
||||
{
|
||||
private const int DefaultSlotCount = 7;
|
||||
|
||||
[Header("Label")]
|
||||
[SerializeField] private string buildName = "새 빌드";
|
||||
|
||||
[Header("Stats")]
|
||||
[Min(0f)] [SerializeField] private float strength = 10f;
|
||||
[Min(0f)] [SerializeField] private float dexterity = 10f;
|
||||
[Min(0f)] [SerializeField] private float intelligence = 10f;
|
||||
[Min(0f)] [SerializeField] private float vitality = 10f;
|
||||
[Min(0f)] [SerializeField] private float wisdom = 10f;
|
||||
[Min(0f)] [SerializeField] private float spirit = 10f;
|
||||
|
||||
[Header("Build Assets")]
|
||||
[SerializeField] private WeaponData weapon;
|
||||
|
||||
[Header("Direct Build")]
|
||||
[Tooltip("직접 구성한 스킬/젬 슬롯입니다. 비어 있으면 로드아웃 프리셋을 사용합니다.")]
|
||||
[SerializeField] private SkillLoadoutEntry[] directSkillSlots = new SkillLoadoutEntry[DefaultSlotCount];
|
||||
[Tooltip("직접 구성한 패시브 트리입니다. 비어 있으면 패시브 프리셋의 트리를 사용합니다.")]
|
||||
[SerializeField] private PassiveTreeData passiveTree;
|
||||
[Tooltip("직접 선택한 패시브 노드 목록입니다. 비어 있으면 패시브 프리셋의 선택 노드를 사용합니다.")]
|
||||
[SerializeField] private List<PassiveNodeData> selectedPassiveNodes = new List<PassiveNodeData>();
|
||||
|
||||
[Header("Preset Fallback")]
|
||||
[SerializeField] private PassivePresetData passivePreset;
|
||||
[SerializeField] private PlayerLoadoutPreset loadoutPreset;
|
||||
|
||||
public string BuildName => string.IsNullOrWhiteSpace(buildName) ? "새 빌드" : buildName.Trim();
|
||||
public float Strength => strength;
|
||||
public float Dexterity => dexterity;
|
||||
public float Intelligence => intelligence;
|
||||
public float Vitality => vitality;
|
||||
public float Wisdom => wisdom;
|
||||
public float Spirit => spirit;
|
||||
public WeaponData Weapon => weapon;
|
||||
public PassiveTreeData PassiveTree => passiveTree;
|
||||
public IReadOnlyList<PassiveNodeData> SelectedPassiveNodes => selectedPassiveNodes;
|
||||
public PassivePresetData PassivePreset => passivePreset;
|
||||
public PlayerLoadoutPreset LoadoutPreset => loadoutPreset;
|
||||
|
||||
/// <summary>
|
||||
/// 입력 상태를 읽기 쉬운 라벨로 구성합니다.
|
||||
/// </summary>
|
||||
public string BuildLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
string passiveName = ResolvePassiveLabel();
|
||||
string loadoutName = ResolveLoadoutLabel();
|
||||
return $"{BuildName} | Passive={passiveName} | Loadout={loadoutName}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 직접 구성 슬롯과 프리셋을 합쳐 실제 계산에 사용할 슬롯 배열을 반환합니다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SkillLoadoutEntry> ResolveLoadoutEntries()
|
||||
{
|
||||
EnsureDirectSlotCapacity();
|
||||
|
||||
if (HasDirectSkillSetup())
|
||||
return directSkillSlots;
|
||||
|
||||
return loadoutPreset != null ? loadoutPreset.Slots : System.Array.Empty<SkillLoadoutEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 계산 가능한 기반 스킬이 하나 이상 있는지 반환합니다.
|
||||
/// </summary>
|
||||
public bool HasAnyResolvedSkill()
|
||||
{
|
||||
IReadOnlyList<SkillLoadoutEntry> entries = ResolveLoadoutEntries();
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
if (entries[i] != null && entries[i].BaseSkill != null)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 계산에 사용할 패시브 트리를 반환합니다.
|
||||
/// </summary>
|
||||
public PassiveTreeData ResolvePassiveTree()
|
||||
{
|
||||
if (passiveTree != null)
|
||||
return passiveTree;
|
||||
|
||||
return passivePreset != null ? passivePreset.Tree : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 계산에 사용할 패시브 노드 ID 목록을 반환합니다.
|
||||
/// </summary>
|
||||
public List<string> BuildSelectedPassiveNodeIdList()
|
||||
{
|
||||
List<string> nodeIds = new List<string>();
|
||||
|
||||
IReadOnlyList<PassiveNodeData> resolvedNodes = ResolveSelectedPassiveNodes();
|
||||
for (int i = 0; i < resolvedNodes.Count; i++)
|
||||
{
|
||||
PassiveNodeData node = resolvedNodes[i];
|
||||
if (node == null || string.IsNullOrWhiteSpace(node.NodeId))
|
||||
continue;
|
||||
|
||||
nodeIds.Add(node.NodeId);
|
||||
}
|
||||
return nodeIds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실제 계산에 사용할 패시브 노드 목록을 반환합니다.
|
||||
/// </summary>
|
||||
public IReadOnlyList<PassiveNodeData> ResolveSelectedPassiveNodes()
|
||||
{
|
||||
if (selectedPassiveNodes != null && selectedPassiveNodes.Count > 0)
|
||||
return selectedPassiveNodes;
|
||||
|
||||
return passivePreset != null ? passivePreset.SelectedNodes : System.Array.Empty<PassiveNodeData>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 직접 구성 슬롯을 덮어씁니다.
|
||||
/// </summary>
|
||||
public void SetDirectSkillSlots(IReadOnlyList<SkillLoadoutEntry> entries)
|
||||
{
|
||||
EnsureDirectSlotCapacity();
|
||||
|
||||
for (int i = 0; i < directSkillSlots.Length; i++)
|
||||
{
|
||||
directSkillSlots[i] = null;
|
||||
}
|
||||
|
||||
if (entries == null)
|
||||
return;
|
||||
|
||||
int copyCount = Mathf.Min(entries.Count, directSkillSlots.Length);
|
||||
for (int i = 0; i < copyCount; i++)
|
||||
{
|
||||
SkillLoadoutEntry entry = entries[i];
|
||||
directSkillSlots[i] = entry != null ? entry.CreateCopy() : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 직접 구성 패시브를 덮어씁니다.
|
||||
/// </summary>
|
||||
public void SetDirectPassiveSelection(PassiveTreeData tree, IReadOnlyList<PassiveNodeData> nodes)
|
||||
{
|
||||
passiveTree = tree;
|
||||
selectedPassiveNodes.Clear();
|
||||
|
||||
if (nodes == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
if (nodes[i] != null)
|
||||
selectedPassiveNodes.Add(nodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 빌드 이름을 갱신합니다.
|
||||
/// </summary>
|
||||
public void SetBuildName(string value)
|
||||
{
|
||||
buildName = value ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 입력값의 복사본을 생성합니다.
|
||||
/// </summary>
|
||||
public BuildSimulationInput CreateCopy()
|
||||
{
|
||||
EnsureDirectSlotCapacity();
|
||||
|
||||
BuildSimulationInput copy = new BuildSimulationInput
|
||||
{
|
||||
buildName = buildName,
|
||||
strength = strength,
|
||||
dexterity = dexterity,
|
||||
intelligence = intelligence,
|
||||
vitality = vitality,
|
||||
wisdom = wisdom,
|
||||
spirit = spirit,
|
||||
weapon = weapon,
|
||||
passiveTree = passiveTree,
|
||||
passivePreset = passivePreset,
|
||||
loadoutPreset = loadoutPreset,
|
||||
};
|
||||
|
||||
copy.directSkillSlots = new SkillLoadoutEntry[directSkillSlots.Length];
|
||||
for (int i = 0; i < directSkillSlots.Length; i++)
|
||||
{
|
||||
copy.directSkillSlots[i] = directSkillSlots[i] != null ? directSkillSlots[i].CreateCopy() : null;
|
||||
}
|
||||
|
||||
copy.selectedPassiveNodes = new List<PassiveNodeData>(selectedPassiveNodes.Count);
|
||||
for (int i = 0; i < selectedPassiveNodes.Count; i++)
|
||||
{
|
||||
if (selectedPassiveNodes[i] != null)
|
||||
copy.selectedPassiveNodes.Add(selectedPassiveNodes[i]);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
private bool HasDirectSkillSetup()
|
||||
{
|
||||
if (directSkillSlots == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < directSkillSlots.Length; i++)
|
||||
{
|
||||
if (directSkillSlots[i] != null && directSkillSlots[i].BaseSkill != null)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void EnsureDirectSlotCapacity()
|
||||
{
|
||||
if (directSkillSlots != null && directSkillSlots.Length == DefaultSlotCount)
|
||||
return;
|
||||
|
||||
SkillLoadoutEntry[] resized = new SkillLoadoutEntry[DefaultSlotCount];
|
||||
if (directSkillSlots != null)
|
||||
{
|
||||
int copyCount = Mathf.Min(directSkillSlots.Length, resized.Length);
|
||||
for (int i = 0; i < copyCount; i++)
|
||||
{
|
||||
resized[i] = directSkillSlots[i];
|
||||
}
|
||||
}
|
||||
|
||||
directSkillSlots = resized;
|
||||
}
|
||||
|
||||
private string ResolvePassiveLabel()
|
||||
{
|
||||
if (selectedPassiveNodes != null && selectedPassiveNodes.Count > 0)
|
||||
return BuildPassiveNodeSummary(selectedPassiveNodes);
|
||||
|
||||
if (passivePreset != null && !string.IsNullOrWhiteSpace(passivePreset.PresetName))
|
||||
return passivePreset.PresetName;
|
||||
|
||||
return "패시브 없음";
|
||||
}
|
||||
|
||||
private string ResolveLoadoutLabel()
|
||||
{
|
||||
if (HasDirectSkillSetup())
|
||||
return BuildLoadoutSummary(directSkillSlots);
|
||||
|
||||
if (loadoutPreset != null && !string.IsNullOrWhiteSpace(loadoutPreset.PresetName))
|
||||
return loadoutPreset.PresetName;
|
||||
|
||||
return "로드아웃 없음";
|
||||
}
|
||||
|
||||
private static string BuildPassiveNodeSummary(IReadOnlyList<PassiveNodeData> nodes)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
PassiveNodeData node = nodes[i];
|
||||
if (node == null)
|
||||
continue;
|
||||
|
||||
if (builder.Length > 0)
|
||||
builder.Append('+');
|
||||
|
||||
builder.Append(string.IsNullOrWhiteSpace(node.DisplayName) ? node.name : node.DisplayName);
|
||||
}
|
||||
|
||||
return builder.Length > 0 ? builder.ToString() : "패시브 없음";
|
||||
}
|
||||
|
||||
private static string BuildLoadoutSummary(IReadOnlyList<SkillLoadoutEntry> entries)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < entries.Count; i++)
|
||||
{
|
||||
SkillLoadoutEntry entry = entries[i];
|
||||
if (entry == null || entry.BaseSkill == null)
|
||||
continue;
|
||||
|
||||
if (builder.Length > 0)
|
||||
builder.Append(" / ");
|
||||
|
||||
builder.Append('S');
|
||||
builder.Append(i);
|
||||
builder.Append('=');
|
||||
builder.Append(entry.BaseSkill.SkillName);
|
||||
|
||||
bool hasGem = false;
|
||||
IReadOnlyList<SkillGemData> gems = entry.SocketedGems;
|
||||
for (int gemIndex = 0; gemIndex < gems.Count; gemIndex++)
|
||||
{
|
||||
SkillGemData gem = gems[gemIndex];
|
||||
if (gem == null)
|
||||
continue;
|
||||
|
||||
builder.Append(hasGem ? '+' : '[');
|
||||
builder.Append(gem.GemName);
|
||||
hasGem = true;
|
||||
}
|
||||
|
||||
if (hasGem)
|
||||
builder.Append(']');
|
||||
}
|
||||
|
||||
return builder.Length > 0 ? builder.ToString() : "직접구성";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user