ThreeKingdomsSlingshot/Assets/Scripts/Runtime/BalanceConfig.cs

147 lines
6.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)")]
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 · #1080)")]
[Tooltip("궁병 사거리 = 슬링샷 최대 거리(얇은 원 · 안의 적은 제자리 사격). 그 밖 ~ 사거리 × (1 + 비율) 안(붉은 도넛)의 적은 사거리에 들어올 만큼만 다가가 사격 · 더 멀면 목표점까지 이동")]
public float archerRangeRatio = 0.2f;
[Tooltip("궁병이 근접 유닛(보병·기병·창병)과 근접 전투(돌진·반격·관통·견제·밀려 닿은 공격)할 때 주는 피해 배율 · 원거리 사격은 1")]
public float archerMeleeDealtRatio = 0.5f;
[Tooltip("궁병이 근접 유닛에게 근접으로 맞을 때 받는 피해 배율(상성 배율과 곱)")]
public float archerMeleeTakenRatio = 1.5f;
[Header("충돌 (PD 2026-11-04 · #1080)")]
[Tooltip("보병 알까기: 부딪힌 유닛(아군·적군)이 밀려나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 · 창병은 비율 없이 남은 거리만큼 밀며 함께 전진")]
public float collisionPushRatio = 1f;
[Header("표시")]
[Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")]
public float unitArtHeight = 1.5f;
[Tooltip("Resources/Board/<이름> 전장 배경 스프라이트(원화 세션 납품 · 5:8) · 비면 도형·격자 바닥")]
public string boardArt = "battlefield_01";
[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;
}
}
}