ThreeKingdomsSlingshot/Assets/Scripts/Runtime/BalanceConfig.cs

137 lines
5.6 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using UnityEngine;
namespace TKS
{
/// <summary>
/// 밸런스·스테이지 데이터의 단일 출처(SOT). 코드에는 수치를 두지 않고 이 자산만 읽는다.
/// 기본값은 에디터 셋업(ProjectSetup)이 최초 1회 시드한다.
/// </summary>
[CreateAssetMenu(menuName = "TKS/Balance Config", fileName = "BalanceConfig")]
public class BalanceConfig : ScriptableObject
{
[Serializable]
public class UnitStats
{
public UnitType type;
public int maxHp;
public int atk;
public int def;
[Tooltip("슬링샷으로 지정할 수 있는 최대 거리(월드 단위)")]
public float slingMaxDistance;
[Tooltip("창병 대기(견제) 반경 · 궁병은 쓰지 않음(사거리 = slingMaxDistance × archerRangeRatio)")]
public float attackRange;
public float radius;
}
[Serializable]
public class HeroDef
{
public string name;
public UnitType type;
public float hpMult = 1f;
public float atkMult = 1f;
[Tooltip("Resources/Units/<art>/ 아트 폴더 이름 · 비면 도형으로 표시")]
public string art = "";
}
[Serializable]
public class EnemySpawn
{
public string name;
public UnitType type;
public Vector2 position;
public float hpMult = 1f;
public float atkMult = 1f;
public bool isBoss;
[Tooltip("Resources/Units/<art>/ 아트 폴더 이름 · 비면 도형으로 표시")]
public string art = "";
}
[Serializable]
public class StageDef
{
public string title;
public string subtitle;
[Range(0f, 1f)] public float aiAccuracy = 0.6f;
[Tooltip("이 거리 안에 아군이 들어와야 적이 움직인다(넓은 전장에서 부대별 교전)")]
public float aiAggroRange = 11f;
public List<EnemySpawn> enemies = new List<EnemySpawn>();
}
[Header("병종 기본 능력치")]
public UnitStats[] unitStats = new UnitStats[0];
[Header("상성 배율 (행 = 공격자, 열 = 피격자 · 순서 보병·기병·궁병·창병)")]
public float[] matchup = new float[UnitTypeInfo.Count * UnitTypeInfo.Count];
[Header("전투 공식")]
[Tooltip("피해 = 공격 × 상성 × (크리 배율) − 방어 × defenseFactor")]
public float defenseFactor = 0.5f;
[Tooltip("근접 공격을 받은 쪽이 되돌려주는 반격 피해 비율")]
public float counterRatio = 0.5f;
public float critMultiplier = 2f;
[Tooltip("피해 난수 폭 (0.1 = ±10 %)")]
public float damageVariance = 0.1f;
[Tooltip("창병 대기 상태에서 움직이는 적에게 주는 피해 비율")]
public float overwatchDamageRatio = 1f;
public float bossHpMult = 1.5f;
public float bossRadiusMult = 1.25f;
[Tooltip("기병 관통 시 적을 옆으로 밀어내는 거리")]
public float cavalryKnockback = 0.35f;
[Header("궁병 규칙 (PD 2026-11-04)")]
[Tooltip("궁병 사거리 = 슬링샷 최대 거리 × 비율. 조준 방향의 슬링샷 거리 + 사거리 안에 적이 있을 때만 행동 가능(없으면 놓아도 취소) · 적 가장자리가 사거리에 들어올 때까지만 이동 후 사격")]
public float archerRangeRatio = 0.2f;
[Header("표시")]
[Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")]
public float unitArtHeight = 1.5f;
[Header("조작")]
[Tooltip("이보다 짧게 당기면 취소")]
public float minDragDistance = 0.35f;
[Tooltip("슬링샷 최종 목표점이 적 원 안에 들어왔다고 보는 여유")]
public float finalHitTolerance = 0.15f;
public float moveSpeed = 9f;
[Header("전장·카메라 (화면보다 넓은 전장을 스크롤)")]
public Vector2 boardSize = new Vector2(20f, 32f);
[Tooltip("화면 세로에 보이는 월드 높이")]
public float cameraViewHeight = 18f;
public float cameraMinViewHeight = 10f;
public float cameraMaxViewHeight = 36f;
public Vector2[] playerSpawnPoints = new Vector2[0];
[Header("아군 영웅 (최대 5)")]
public HeroDef[] heroes = new HeroDef[0];
[Header("스테이지")]
public StageDef[] stages = new StageDef[0];
public UnitStats GetStats(UnitType t)
{
for (int i = 0; i < unitStats.Length; i++)
{
if (unitStats[i].type == t) return unitStats[i];
}
throw new InvalidOperationException("BalanceConfig 에 " + t + " 능력치가 없습니다.");
}
public float Matchup(UnitType attacker, UnitType defender)
{
int idx = (int)attacker * UnitTypeInfo.Count + (int)defender;
if (matchup == null || idx >= matchup.Length) return 1f;
return matchup[idx];
}
public void SetMatchup(UnitType attacker, UnitType defender, float value)
{
if (matchup == null || matchup.Length != UnitTypeInfo.Count * UnitTypeInfo.Count)
matchup = new float[UnitTypeInfo.Count * UnitTypeInfo.Count];
matchup[(int)attacker * UnitTypeInfo.Count + (int)defender] = value;
}
}
}