2026-11-03 19:10:10 +00:00
using System ;
using System.Collections.Generic ;
using UnityEngine ;
namespace TKS
{
2026-11-05 03:17:19 +00:00
public enum StageKind
{
Normal = 0 ,
Elite = 1 , // 이름 있는 무장이 나오는 일반 스테이지(무장 isBoss = false · 배율만 보정)
Boss = 2 // 5·10·15·20
}
2026-11-03 19:10:10 +00:00
/// <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 ;
2026-11-10 08:25:47 +00:00
[Tooltip("옛 창병 대기(견제) 반경 · 견제 제거(PD 2026-11-10)로 지금은 쓰지 않음 · 궁병도 쓰지 않음(사거리 = slingMaxDistance)")]
2026-11-03 19:10:10 +00:00
public float attackRange ;
public float radius ;
}
[Serializable]
public class EnemySpawn
{
public string name ;
public UnitType type ;
public Vector2 position ;
public float hpMult = 1f ;
public float atkMult = 1f ;
public bool isBoss ;
2026-11-05 04:23:01 +00:00
[Tooltip("이름 있는 무장(스테이지 표 generals 열 · 보스·엘리트) · false = 병사")]
public bool isGeneral ;
2026-11-04 04:05:46 +00:00
[Tooltip("Resources/Units/<art>/ 아트 폴더 이름 · 비면 도형으로 표시")]
public string art = "" ;
2026-11-03 19:10:10 +00:00
}
[Serializable]
public class StageDef
{
public string title ;
public string subtitle ;
[Range(0f, 1f)] public float aiAccuracy = 0.6f ;
2026-11-03 19:29:16 +00:00
[Tooltip("이 거리 안에 아군이 들어와야 적이 움직인다(넓은 전장에서 부대별 교전)")]
public float aiAggroRange = 11f ;
2026-11-03 19:10:10 +00:00
public List < EnemySpawn > enemies = new List < EnemySpawn > ( ) ;
2026-11-05 03:17:19 +00:00
[Header("스테이지 표(Assets/Data/Stages/stages.csv · TKS/Import Stages)")]
public int chapter ;
[Tooltip("챕터 안 번호(1부터)")]
public int stage ;
public StageKind kind ;
public string place ;
[Tooltip("factions.csv 세력 코드")]
public string faction ;
public string bgTheme ;
public string note ;
}
[Serializable]
public class ChapterDef
{
public int no ;
public string title ;
public string subtitle ;
public string years ;
[Tooltip("Resources/ChapterMap/Maps/<map> 맵 그림 · Data/<map>.json 노드 자리(slots_N)")]
public string map ;
}
/// <summary>적 배치 규칙(스테이지 표의 병사 수 → 전위·중군·후군 좌표 · 임포트 때 적용).</summary>
[Serializable]
public class FormationRule
{
[Tooltip("전위(보병·창병 우선) 첫 줄 y")]
public float frontY = - 5.5f ;
[Tooltip("중군(궁병 가운데 · 기병 양 끝) 첫 줄 y")]
public float middleY = 0.5f ;
[Tooltip("후군 경비(창병·기병 우선) 줄 y")]
public float rearY = 7.5f ;
[Tooltip("장수(보스·엘리트) 줄 y · 보스가 가운데")]
public float commandY = 11f ;
public float colGap = 2.6f ;
[Tooltip("같은 대가 여러 줄일 때 다음 줄 간격(적 쪽으로)")]
public float rowGap = 1.8f ;
public int maxPerRow = 5 ;
[Tooltip("여러 줄일 때 줄마다 좌우로 엇갈리는 양(× colGap)")]
public float rowStagger = 0.5f ;
[Range(0f, 1f)] public float frontShare = 0.4f ;
[Range(0f, 1f)] public float rearShare = 0.15f ;
2026-11-03 19:10:10 +00:00
}
[Header("병종 기본 능력치")]
public UnitStats [ ] unitStats = new UnitStats [ 0 ] ;
2026-11-05 04:06:32 +00:00
[Header("상성 배율 (행 = 공격자, 열 = 피격자 · 순서 보병·기병·궁병·창병·책사 · 5× 5)")]
2026-11-03 19:10:10 +00:00
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 ;
2026-11-04 06:56:02 +00:00
[Header("궁병 규칙 (PD 2026-11-04 · #1080)")]
[Tooltip("궁병 사거리 = 슬링샷 최대 거리(얇은 원 · 안의 적은 제자리 사격). 그 밖 ~ 사거리 × (1 + 비율) 안(붉은 도넛)의 적은 사거리에 들어올 만큼만 다가가 사격 · 더 멀면 목표점까지 이동")]
2026-11-04 04:05:46 +00:00
public float archerRangeRatio = 0.2f ;
2026-11-04 06:56:02 +00:00
[Tooltip("궁병이 근접 유닛(보병·기병·창병)과 근접 전투(돌진·반격·관통·견제·밀려 닿은 공격)할 때 주는 피해 배율 · 원거리 사격은 1")]
public float archerMeleeDealtRatio = 0.5f ;
[Tooltip("궁병이 근접 유닛에게 근접으로 맞을 때 받는 피해 배율(상성 배율과 곱)")]
public float archerMeleeTakenRatio = 1.5f ;
2026-11-05 04:06:32 +00:00
[Tooltip("화살 투사체 반지름(PD 2026-11-05 #1087 ⑥) — 타겟 방향 원 캐스트 폭 · 경로 위 첫 유닛(아군 포함)이 맞는다 · 관통 없음 · 책사 투사체도 같음 · 근접 패널티는 책사도 위 두 배율")]
public float arrowRadius = 0.1f ;
[Header("책사 광역 (PD 2026-11-05 · #1087 ⑤)")]
[Tooltip("투사체가 맞힌 중심 유닛 위치에서 이 반경 원에 몸이 닿는 다른 유닛 전원(아군 포함 · 쏜 책사 제외)이 광역 피해")]
public float strategistSplashRadius = 1.5f ;
[Tooltip("광역 피해 = 대상별 피해(상성 적용 · 크리 없음) × 비율")]
public float strategistSplashRatio = 0.6f ;
[Tooltip("광역에 휩쓸린 같은 편(쏜 책사의 아군)이 받는 배율 · 1 = 적과 동일 · 0 = 아군 무피해")]
public float strategistAllyDamageRatio = 1f ;
2026-11-04 06:56:02 +00:00
[Header("충돌 (PD 2026-11-04 · #1080)")]
[Tooltip("보병 알까기: 부딪힌 유닛(아군·적군)이 밀려나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 · 창병은 비율 없이 남은 거리만큼 밀며 함께 전진")]
public float collisionPushRatio = 1f ;
2026-11-04 08:32:07 +00:00
[Tooltip("보병 알까기: 부딪힌 보병 자신이 반대 방향으로 튕겨나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 (PD #1081 「자신도 튕겨나야」)")]
public float collisionSelfBounceRatio = 0.5f ;
2026-11-10 08:25:47 +00:00
[Tooltip("창병 밀치기(PD 2026-11-10 「창병 = 적을 크게 밀치지만 자신은 적게 밀린다」): 상대가 밀리는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 × 밀기 배율(철척사모) · 보병(collisionPushRatio)보다 크게")]
public float spearPushRatio = 1.5f ;
[Tooltip("창병 밀치기 반작용(PD 2026-11-06 #1098 · 2026-11-10 「자신은 적게 밀린다」): 상대가 밀린 거리 × 비율만큼 창병이 반대로 밀린다(벽·유닛에 닿으면 정지 · 피해 없음) · 보병 자기 튕김(collisionSelfBounceRatio)보다 작게")]
public float spearRecoilRatio = 0.2f ;
2026-11-04 04:05:46 +00:00
[Header("표시")]
[Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")]
public float unitArtHeight = 1.5f ;
2026-11-09 10:33:00 +00:00
[Tooltip("말 탄 유닛(Resources/Units/art_size.csv mounted = 1)의 그려진 머리 끝 상한 = 표준 키 × 이 값 — 크기 표대로면 탄 사람 = 보병 크기라 1.4~1.8배로 커짐 → PD 2026-11-09 「완벽한 비율 아니어도 됨 · 보병보다 조금 크게 · 너무 커지지 않게」 · 0 = 상한 없음")]
public float mountedArtMaxTop = 1.3f ;
2026-11-04 06:56:02 +00:00
[Tooltip("Resources/Board/<이름> 전장 배경 스프라이트(원화 세션 납품 · 5:8) · 비면 도형·격자 바닥")]
public string boardArt = "battlefield_01" ;
2026-11-04 04:05:46 +00:00
2026-11-03 19:10:10 +00:00
[Header("조작")]
[Tooltip("이보다 짧게 당기면 취소")]
public float minDragDistance = 0.35f ;
[Tooltip("슬링샷 최종 목표점이 적 원 안에 들어왔다고 보는 여유")]
public float finalHitTolerance = 0.15f ;
public float moveSpeed = 9f ;
2026-11-03 19:29:16 +00:00
[Header("전장·카메라 (화면보다 넓은 전장을 스크롤)")]
public Vector2 boardSize = new Vector2 ( 20f , 32f ) ;
[Tooltip("화면 세로에 보이는 월드 높이")]
public float cameraViewHeight = 18f ;
public float cameraMinViewHeight = 10f ;
public float cameraMaxViewHeight = 36f ;
2026-11-05 12:11:10 +00:00
[Tooltip("아군 출발 자리(편성 순서) · 0~2 = 전열(앞줄) · 3~4 = 후열(뒷줄) · 아군 영웅·병사 정의 = 아웃게임 HeroDb(Assets/Data/OutGame/heroes.csv · 2026-11-05 #1093 · 구 heroes[] 대 체 ) ")]
2026-11-03 19:10:10 +00:00
public Vector2 [ ] playerSpawnPoints = new Vector2 [ 0 ] ;
2026-11-05 03:17:19 +00:00
[Header("스테이지 (Assets/Data/Stages/*.csv → 메뉴 TKS/Import Stages · 직접 고치면 다음 임포트에 덮임)")]
public ChapterDef [ ] chapters = new ChapterDef [ 0 ] ;
2026-11-03 19:10:10 +00:00
public StageDef [ ] stages = new StageDef [ 0 ] ;
2026-11-05 03:17:19 +00:00
[Header("스테이지 임포트 규칙 (값을 바꾸면 다시 임포트)")]
public FormationRule formation = new FormationRule ( ) ;
[Tooltip("보스 스테이지 장수 배율(generals.csv hp_mult·atk_mult 가 비었을 때) · isBoss 라 bossHpMult 가 더 곱해진다")]
public float bossGeneralHpMult = 1.5f ;
public float bossGeneralAtkMult = 1.3f ;
[Tooltip("엘리트 장수 배율(isBoss 아님 · generals.csv 가 비었을 때)")]
public float eliteGeneralHpMult = 1.25f ;
public float eliteGeneralAtkMult = 1.15f ;
2026-11-03 19:10:10 +00:00
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 + " 능력치가 없습니다." ) ;
}
2026-11-05 03:17:19 +00:00
/// <summary>챕터 no 의 스테이지 인덱스(stages 순서 = 표 순서).</summary>
public List < int > StagesOfChapter ( int no )
{
var res = new List < int > ( ) ;
for ( int i = 0 ; i < stages . Length ; i + + )
{
if ( stages [ i ] . chapter = = no ) res . Add ( i ) ;
}
return res ;
}
2026-11-03 19:10:10 +00:00
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 ;
}
}
}