ThreeKingdomsSlingshot/Assets/Scripts/Runtime/BalanceConfig.cs

226 lines
12 KiB
C#
Raw Permalink 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
{
public enum StageKind
{
Normal = 0,
Elite = 1, // 이름 있는 무장이 나오는 일반 스테이지(무장 isBoss = false · 배율만 보정)
Boss = 2 // 5·10·15·20
}
/// <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("옛 창병 대기(견제) 반경 · 견제 제거(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/<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("스테이지 표(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;
}
[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 + " 능력치가 없습니다.");
}
/// <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;
}
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;
}
}
}