using System; using System.Collections.Generic; using UnityEngine; namespace TKS { public enum StageKind { Normal = 0, Elite = 1, // 이름 있는 무장이 나오는 일반 스테이지(무장 isBoss = false · 배율만 보정) Boss = 2 // 5·10·15·20 } /// /// 밸런스·스테이지 데이터의 단일 출처(SOT). 코드에는 수치를 두지 않고 이 자산만 읽는다. /// 기본값은 에디터 셋업(ProjectSetup)이 최초 1회 시드한다. /// [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("옛 창병 대기(견제) 반경 · 견제 제거(PD 2026-11-10)로 지금은 쓰지 않음 · 궁병도 쓰지 않음(사거리 = slingMaxDistance)")] 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; [Tooltip("이름 있는 무장(스테이지 표 generals 열 · 보스·엘리트) · false = 병사")] public bool isGeneral; [Tooltip("Resources/Units// 아트 폴더 이름 · 비면 도형으로 표시")] 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 enemies = new List(); [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/ 맵 그림 · Data/.json 노드 자리(slots_N)")] public string map; } /// 적 배치 규칙(스테이지 표의 병사 수 → 전위·중군·후군 좌표 · 임포트 때 적용). [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; } [Header("병종 기본 능력치")] public UnitStats[] unitStats = new UnitStats[0]; [Header("상성 배율 (행 = 공격자, 열 = 피격자 · 순서 보병·기병·궁병·창병·책사 · 5×5)")] 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; [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; [Header("충돌 (PD 2026-11-04 · #1080)")] [Tooltip("보병 알까기: 부딪힌 유닛(아군·적군)이 밀려나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 · 창병은 비율 없이 남은 거리만큼 밀며 함께 전진")] public float collisionPushRatio = 1f; [Tooltip("보병 알까기: 부딪힌 보병 자신이 반대 방향으로 튕겨나는 거리 = 충돌 시 남은 슬링샷 거리 × 비율 (PD #1081 「자신도 튕겨나야」)")] public float collisionSelfBounceRatio = 0.5f; [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; [Header("표시")] [Tooltip("유닛 아트(스프라이트) 세로 크기 · 월드 단위 · 장수는 반지름 배율만큼 커진다")] public float unitArtHeight = 1.5f; [Tooltip("말 탄 유닛(Resources/Units/art_size.csv mounted = 1)의 그려진 머리 끝 상한 = 표준 키 × 이 값 — 크기 표대로면 탄 사람 = 보병 크기라 1.4~1.8배로 커짐 → PD 2026-11-09 「완벽한 비율 아니어도 됨 · 보병보다 조금 크게 · 너무 커지지 않게」 · 0 = 상한 없음")] public float mountedArtMaxTop = 1.3f; [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; [Tooltip("아군 출발 자리(편성 순서) · 0~2 = 전열(앞줄) · 3~4 = 후열(뒷줄) · 아군 영웅·병사 정의 = 아웃게임 HeroDb(Assets/Data/OutGame/heroes.csv · 2026-11-05 #1093 · 구 heroes[] 대체)")] public Vector2[] playerSpawnPoints = new Vector2[0]; [Header("스테이지 (Assets/Data/Stages/*.csv → 메뉴 TKS/Import Stages · 직접 고치면 다음 임포트에 덮임)")] public ChapterDef[] chapters = new ChapterDef[0]; public StageDef[] stages = new StageDef[0]; [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; 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 + " 능력치가 없습니다."); } /// 챕터 no 의 스테이지 인덱스(stages 순서 = 표 순서). public List StagesOfChapter(int no) { var res = new List(); for (int i = 0; i < stages.Length; i++) { if (stages[i].chapter == no) res.Add(i); } return res; } 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; } } }