diff --git a/Assets/Scripts/Runtime/ActionResolver.cs b/Assets/Scripts/Runtime/ActionResolver.cs index ac7a8f77..ed12e804 100644 --- a/Assets/Scripts/Runtime/ActionResolver.cs +++ b/Assets/Scripts/Runtime/ActionResolver.cs @@ -12,7 +12,6 @@ namespace TKS public bool Overwatch; // 창병: 최종 목표점 명중 → 대기(견제) public bool Crit; // 궁병: 최종 목표점 명중 → 크리티컬 public bool MovedOnly; // 공격 없이 이동만 - public bool Cancelled; // 궁병: 조준 방향 슬링샷 범위 + 사거리 안에 적이 없어 행동하지 않음 public int DamageDealt; public int CounterTaken; public List Attacked = new List(); @@ -25,8 +24,9 @@ namespace TKS /// ① 보병: 경로 위 첫 적까지 이동 후 공격 · 목표점 명중 시 추가 이동 1회 /// ② 기병: 경로 위 적을 모두 관통 피해 · 목표점까지 이동 · 목표점 명중 시 제자리 복귀 /// ③ 창병: 보병과 동일 · 목표점 명중 시 대기 → 반경 안에서 움직이는 적에게 피해 - /// ④ 궁병: 조준 방향 슬링샷 거리 + 사거리(슬링샷 최대 × archerRangeRatio) 안에 적이 있을 때만 행동 — - /// 적 가장자리가 사거리에 들어올 때까지만 이동 후 사격(반격 없음) · 목표점 명중 시 크리티컬 · 적이 없으면 취소(PD 2026-11-04) + /// ④ 궁병: 조준 방향 슬링샷 거리 + 사거리(슬링샷 최대 × archerRangeRatio) 안에 적이 있으면 + /// 적 가장자리가 사거리에 들어올 때까지만 이동 후 사격(반격 없음) · 목표점 명중 시 크리티컬 · + /// 적이 없으면 목표점까지 이동만(PD 2026-11-04 2차 지시 · 1차의 「행동 취소」 폐기) /// public class ActionResolver { @@ -251,23 +251,18 @@ namespace TKS IEnumerator ArcherRoutine(Unit u, Vector2 origin, Vector2 dir, float dist, Vector2 target, bool moveOnly, ActionOutcome o) { - if (moveOnly) + // 조준 방향으로 슬링샷 거리 + 사거리 안의 첫 적. hitDist = 그 적 가장자리에 닿기까지의 이동 거리. + float hitDist = 0f; + Unit first = moveOnly ? null : Combat.FirstEnemyOnPath(u, origin, dir, dist + u.AttackRange, out hitDist); + if (first == null) { + // 적이 없으면 목표점까지 이동(PD 2026-11-04 2차: 「슬링샷 범위 내 적이 없을 경우 해당 위치까지 이동」) float free = Combat.FreeTravel(u, origin, dir, dist, true); yield return Move(u, origin + dir * free, o); o.MovedOnly = true; yield break; } - // 조준 방향으로 슬링샷 거리 + 사거리 안의 첫 적. hitDist = 그 적 가장자리에 닿기까지의 이동 거리. - float hitDist; - Unit first = Combat.FirstEnemyOnPath(u, origin, dir, dist + u.AttackRange, out hitDist); - if (first == null) - { - o.Cancelled = true; // PD 2026-11-04: 슬링샷 범위 + 비율 안에 적이 있을 때만 행동 가능 - yield break; - } - // 적 가장자리까지 남은 간격이 사거리 안에 들어올 때까지만 이동(슬링샷 거리 상한) float need = Mathf.Clamp(hitDist - u.AttackRange, 0f, dist); float freeMove = Combat.FreeTravel(u, origin, dir, need, true); diff --git a/Assets/Scripts/Runtime/BattleManager.cs b/Assets/Scripts/Runtime/BattleManager.cs index 64229d04..58e1aeec 100644 --- a/Assets/Scripts/Runtime/BattleManager.cs +++ b/Assets/Scripts/Runtime/BattleManager.cs @@ -383,7 +383,6 @@ namespace TKS static string Describe(Unit u, ActionOutcome o, bool moveOnly) { string who = u.DisplayName + "(" + UnitTypeInfo.Korean(u.Type) + ")"; - if (o.Cancelled) return who + " 사거리 안에 적이 없어 대기"; if (o.Attacked.Count == 0) { return who + (moveOnly ? " 추가 이동" : " 이동"); diff --git a/Assets/Scripts/Runtime/SlingshotInput.cs b/Assets/Scripts/Runtime/SlingshotInput.cs index 335c11d6..ad6648c7 100644 --- a/Assets/Scripts/Runtime/SlingshotInput.cs +++ b/Assets/Scripts/Runtime/SlingshotInput.cs @@ -7,13 +7,12 @@ namespace TKS /// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격. /// 유닛 탭(아군·적군) = 그 유닛의 사거리 표시(RangeIndicator) · 빈 곳 드래그 = 전장 스크롤 · 휠 = 줌 · 미니맵 탭 = 그 지점으로 이동. /// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기. - /// 궁병(PD 2026-11-04): 조준 방향 슬링샷 거리 + 사거리 안에 적이 없으면 놓아도 행동하지 않는다(선택 유지 · 안내 문구). + /// 궁병도 다른 병종과 같이 놓으면 바로 행동한다(조준 방향에 적이 없으면 목표점까지 이동 · PD 2026-11-04 2차 지시). /// public class SlingshotInput : MonoBehaviour { static readonly Color TargetFreeColor = Color.white; static readonly Color TargetHitColor = new Color(1f, 0.6f, 0.1f); - static readonly Color TargetBlockedColor = new Color(0.65f, 0.65f, 0.65f, 0.7f); BattleManager battle; Camera cam; @@ -26,7 +25,6 @@ namespace TKS bool panning; bool emptyPress; bool previewValid; - bool archerBlocked; Vector2 lastScreen; float panPixels; Vector2 currentTarget; @@ -124,7 +122,6 @@ namespace TKS panning = false; emptyPress = false; previewValid = false; - archerBlocked = false; Vector2 mapWorld; if (hud != null && camCtrl != null && hud.TryMinimapTap(screen, out mapWorld)) @@ -170,43 +167,24 @@ namespace TKS { HidePreview(); previewValid = false; - archerBlocked = false; return; } if (mag > selected.SlingMaxDistance) launch = launch / mag * selected.SlingMaxDistance; currentTarget = battle.Board.ClampInside(selected.Position + launch, selected.Radius); previewValid = true; - int targets = ShowPreview(selected, currentTarget, w); - archerBlocked = selected.Type == UnitType.Archer && !selected.BonusMoveAvailable && targets == 0; - if (archerBlocked) - { - targetRing.color = TargetBlockedColor; - targetRing.transform.localScale = new Vector3(0.7f, 0.7f, 1f); - } + ShowPreview(selected, currentTarget, w); } void OnRelease(Vector2 screen) { if (dragging && selected != null && previewValid && battle.CanAct(selected)) { - if (archerBlocked) - { - // 궁병: 조준 방향 슬링샷 범위 + 비율 안에 적 없음 → 행동 취소(선택·사거리 표시 유지) - HidePreview(); - if (selected.View != null) selected.View.SetAiming(false); - int pct = Mathf.RoundToInt(battle.Config.archerRangeRatio * 100f); - battle.LastMessage = selected.DisplayName + "(궁병) — 슬링샷 범위 +" + pct + " % 안에 적이 없어 행동할 수 없습니다"; - battle.NotifyChanged(); - } - else - { - var t = currentTarget; - var u = selected; - HidePreview(); - if (u.View != null) u.View.SetAiming(false); - Select(null); - battle.RequestPlayerAction(u, t); - } + var t = currentTarget; + var u = selected; + HidePreview(); + if (u.View != null) u.View.SetAiming(false); + Select(null); + battle.RequestPlayerAction(u, t); } else if (emptyPress && panPixels <= TapPixels) { @@ -221,7 +199,6 @@ namespace TKS panning = false; emptyPress = false; previewValid = false; - archerBlocked = false; } /// 유닛 선택(아군·적군 · null = 해제): 선택 링 + HUD 정보 + 사거리 표시(궁병은 도넛 · 범위 안 적 유무로 색). diff --git a/Assets/Tests/PlayMode/BattleFlowTests.cs b/Assets/Tests/PlayMode/BattleFlowTests.cs index 12a8a5bb..7d1a9a1a 100644 --- a/Assets/Tests/PlayMode/BattleFlowTests.cs +++ b/Assets/Tests/PlayMode/BattleFlowTests.cs @@ -138,7 +138,6 @@ namespace TKS.Tests var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -0.2f), "적보병"); var o = new ActionOutcome(); yield return bm.ResolveForTest(arc, new Vector2(0f, -1f), false, o); - Assert.IsFalse(o.Cancelled); Assert.Contains(foe, o.Attacked, "조준 방향의 적을 사거리까지 이동 후 사격"); Assert.Less(foe.Hp, foe.MaxHp); Assert.AreEqual(arc.MaxHp, arc.Hp, "궁병은 반격을 받지 않는다"); @@ -149,19 +148,20 @@ namespace TKS.Tests } [UnityTest] - public IEnumerator Archer_CannotAct_WhenNoEnemyWithinSlingPlusRatio() + public IEnumerator Archer_MovesToTarget_WhenNoEnemyWithinSlingPlusRatio() { yield return Fresh(); var arc = bm.SpawnUnit(UnitType.Archer, Faction.Player, new Vector2(0f, -5f), "궁병"); - // 적 가장자리까지 6.0 − 반지름 합 ≈ 5.2 > 4.0 + 0.8 → 행동 불가(이동도 없음) + // 적 가장자리까지 6.0 − 반지름 합 ≈ 5.2 > 슬링샷 + 사거리 → 공격 없이 목표점까지 이동(PD 2026-11-04 2차 지시) var foe = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 1f), "적보병"); Assert.IsFalse(bm.AnyEnemyWithinArcherReach(arc), "공격 가능 범위 안에 적 없음(도넛 회색)"); var o = new ActionOutcome(); - yield return bm.ResolveForTest(arc, new Vector2(0f, -1f), false, o); - Assert.IsTrue(o.Cancelled, "슬링샷 범위 + 비율 안에 적이 없으면 궁병은 행동하지 않는다"); + var target = new Vector2(0f, -1f); + yield return bm.ResolveForTest(arc, target, false, o); + Assert.IsTrue(o.MovedOnly, "슬링샷 범위 + 비율 안에 적이 없으면 궁병은 목표점까지 이동만 한다"); Assert.AreEqual(0, o.Attacked.Count); Assert.AreEqual(foe.MaxHp, foe.Hp); - Assert.Less(Vector2.Distance(arc.Position, new Vector2(0f, -5f)), 0.01f, "이동도 하지 않는다"); + Assert.Less(Vector2.Distance(arc.Position, target), 0.05f, "목표 지점까지 이동"); var near = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -0.4f), "가까운적"); Assert.IsTrue(bm.AnyEnemyWithinArcherReach(arc), "범위 안에 적이 생기면 도넛 붉게"); diff --git a/CLAUDE.md b/CLAUDE.md index 336c7596..93291931 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,13 +39,14 @@ Unity.exe -projectPath -executeMethod TKS.EditorTools.ProjectSetup.AutoPl - `Unity.exe` = `C:\Program Files\Unity\Hub\Editor\6000.3.23f1\Editor\Unity.exe`. 에디터가 같은 프로젝트를 열고 있으면 배치 실행은 못 한다(`Temp/UnityLockfile`) — 먼저 `unity status` 로 확인. - 배치 로그 판정 = `error CS` 0 · 테스트 XML `failed="0"`. +- **에디터가 열리면서 크래시하면 oCam(화면 녹화)을 의심**: `%LOCALAPPDATA%\Unity\Editor\Editor.log` 꼬리의 스택에 `HookSurfaceDLL` → `D3D12SwapChain::Present` 가 있으면 oCam 의 D3D12 훅이 원인(2026-11-04 15:04 × 2 실측). 이 프로젝트는 그래서 Windows Graphics API 를 **D3D11 우선**으로 고정했다(`ProjectSettings.asset` `m_BuildTargetGraphicsAPIs` · 에디터도 이 설정을 따름). 그래도 크래시하면 `-force-d3d11` 인수 또는 oCam 종료. 배치 `-nographics` 는 영향 없음. ## 코드 규칙 - 수치·영웅·스테이지 SOT = `Assets/Resources/BalanceConfig.asset`(Inspector 조정 · 최초 시드만 `Assets/Scripts/Editor/BalanceDefaults.cs`). 코드·테스트에 수치를 직접 적지 않는다(C45). - 규칙 해석 변경 = `Assets/Scripts/Runtime/ActionResolver.cs`(병종 4종) · `BattleManager.cs`(턴·승패·창병 대기) · `Combat.cs`(피해 공식·경로 질의). 변경 시 `Assets/Tests/PlayMode/BattleFlowTests.cs` 에 테스트 추가 후 14종 이상 전부 통과 확인. - 콜라이더 질의 전 `Physics2D.SyncTransforms()` · 유닛 제거는 `SetActive(false)` 후 `Destroy` · 자식 렌더러가 있는 표시물은 GameObject 통째로 끈다. -- **궁병 규칙(PD 2026-11-04)**: 사거리 = `slingMaxDistance × archerRangeRatio`(자산 · 기본 0.2 = 슬링샷 범위 +20 %). 조준 방향 「슬링샷 거리 + 사거리」 안에 적이 없으면 행동 불가(`SlingshotInput` 이 놓기 자체를 취소 · AI 는 `ActionOutcome.Cancelled` 로 대기) · 적 **가장자리**가 사거리에 들어올 때까지만 이동 후 사격. `UnitStats.attackRange` 는 창병 견제 반경만. +- **궁병 규칙(PD 2026-11-04)**: 사거리 = `slingMaxDistance × archerRangeRatio`(자산 · 기본 0.2 = 슬링샷 범위 +20 %). 조준 방향 「슬링샷 거리 + 사거리」 안에 적이 있으면 적 **가장자리**가 사거리에 들어올 때까지만 이동 후 사격 · 적이 없으면 **목표점까지 이동만**(PD 2026-11-04 2차 지시 · 1차의 「행동 취소」 폐기 · AI 적 궁병도 같음). `UnitStats.attackRange` 는 창병 견제 반경만. - **사거리 표시(PD 2026-11-04)**: 유닛 탭(아군·적군) → `RangeIndicator`(슬링샷 최대 거리 = 얇은 반투명 원 · 궁병 = 슬링샷 범위 ~ ×(1+비율) 채워진 붉은 도넛 · 범위 안 적이 없으면 회색). - 보고는 실측·추정·미확인을 분리한다(C23 · C5). 커밋은 의도 변경 파일만. diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 5f5b881a..98e6c342 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -395,7 +395,10 @@ PlayerSettings: m_BuildTargetShaderSettings: [] m_BuildTargetGraphicsJobs: [] m_BuildTargetGraphicsJobMode: [] - m_BuildTargetGraphicsAPIs: [] + m_BuildTargetGraphicsAPIs: + - m_BuildTarget: WindowsStandaloneSupport + m_APIs: 0200000012000000 + m_Automatic: 0 m_BuildTargetVRSettings: [] m_DefaultShaderChunkSizeInMB: 16 m_DefaultShaderChunkCount: 0 diff --git a/README.md b/README.md index efa36d43..7292896e 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Unity 6000.3.23f1 · 2D · 도형 플레이스홀더(아트 리소스는 별도 | 보병 | 첫 적까지 이동 후 공격(반격 받음) | 추가 이동 1회(이동만) | | 기병 | 경로 위 모든 적 관통 피해 · 목표점까지 이동 | 제자리로 복귀 | | 창병 | 보병과 동일 | 대기 — 적군 턴에 반경 안에서 움직이는 적에게 피해 | -| 궁병 | 목표 방향의 적을 향해 사거리까지 이동 후 사격(반격 없음) | 무조건 크리티컬 | +| 궁병 | 목표 방향 「슬링샷 거리 + 사거리」 안의 적을 향해 사거리까지 이동 후 사격(반격 없음) · 적이 없으면 목표점까지 이동 | 무조건 크리티컬 | 상성: 보병 > 창병 > 기병 > 보병. 궁병은 모두에게 약함(받는 피해 1.5배).