Files
Colosseum/Assets/_Game/Scripts/Combat/Simulation/BuildSimulationInput.cs
dal4segno 285da31047 feat: 허수아비 계산 시뮬레이터 추가
- 빌드 입력, 룰셋, 회전 정책, 결과/리포트 모델을 포함한 데미지 계산 시뮬레이터 기반을 추가
- 단일 실행 창과 배치 전수 조사 창, 플레이어 데미지 스윕 메뉴를 추가
- DamageEffect 계산값 접근자를 열어 기존 전투 공식을 시뮬레이터에서 재사용하도록 정리
2026-03-28 15:07:09 +09:00

339 lines
12 KiB
C#

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() : "직접구성";
}
}
}