궁병 — 조준 방향에 적이 없으면 목표점까지 이동(PD 2026-11-04 2차 지시 · 「행동 취소」 폐기) · 에디터 그래픽 API D3D11 우선(oCam 훅 D3D12 크래시 우회) · 테스트 17/17

- ActionResolver.ArcherRoutine: 복도(당긴 거리 + 사거리) 안에 적 없음 → FreeTravel 로 목표점까지 이동 · MovedOnly · ActionOutcome.Cancelled 삭제
- SlingshotInput: archerBlocked(놓기 취소 · 회색 목표 링 · HUD 안내 문구) 삭제 · BattleManager.Describe 「사거리 안에 적이 없어 대기」 삭제 · AI 적 궁병도 전진
- 테스트: Archer_CannotAct_… → Archer_MovesToTarget_WhenNoEnemyWithinSlingPlusRatio (사본 배치 error CS 0 · 17/17)
- ProjectSettings: WindowsStandaloneSupport m_APIs D3D11 → D3D12 (PD 에디터 15:04 크래시 2회 = oCam HookSurfaceDLL → D3D12SwapChain::Present · 사본 GUI 실측 Direct3D 11.0 · oCam 켜진 채 자동 플레이 1턴 정상)
- CLAUDE.md(궁병 규칙 · 크래시 진단 절차) · README 병종 표 갱신

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-04 15:16:15 +09:00
parent bc8e571eea
commit 645f8646cf
7 changed files with 29 additions and 54 deletions

View File

@ -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<Unit> Attacked = new List<Unit>();
@ -25,8 +24,9 @@ namespace TKS
/// ① 보병: 경로 위 첫 적까지 이동 후 공격 · 목표점 명중 시 추가 이동 1회
/// ② 기병: 경로 위 적을 모두 관통 피해 · 목표점까지 이동 · 목표점 명중 시 제자리 복귀
/// ③ 창병: 보병과 동일 · 목표점 명중 시 대기 → 반경 안에서 움직이는 적에게 피해
/// ④ 궁병: 조준 방향 슬링샷 거리 + 사거리(슬링샷 최대 × archerRangeRatio) 안에 적이 있을 때만 행동 —
/// 적 가장자리가 사거리에 들어올 때까지만 이동 후 사격(반격 없음) · 목표점 명중 시 크리티컬 · 적이 없으면 취소(PD 2026-11-04)
/// ④ 궁병: 조준 방향 슬링샷 거리 + 사거리(슬링샷 최대 × archerRangeRatio) 안에 적이 있으면
/// 적 가장자리가 사거리에 들어올 때까지만 이동 후 사격(반격 없음) · 목표점 명중 시 크리티컬 ·
/// 적이 없으면 목표점까지 이동만(PD 2026-11-04 2차 지시 · 1차의 「행동 취소」 폐기)
/// </summary>
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);

View File

@ -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 ? " 추가 이동" : " 이동");

View File

@ -7,13 +7,12 @@ namespace TKS
/// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격.
/// 유닛 탭(아군·적군) = 그 유닛의 사거리 표시(RangeIndicator) · 빈 곳 드래그 = 전장 스크롤 · 휠 = 줌 · 미니맵 탭 = 그 지점으로 이동.
/// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기.
/// 궁병(PD 2026-11-04): 조준 방향 슬링샷 거리 + 사거리 안에 적이 없으면 놓아도 행동하지 않는다(선택 유지 · 안내 문구).
/// 궁병도 다른 병종과 같이 놓으면 바로 행동한다(조준 방향에 적이 없으면 목표점까지 이동 · PD 2026-11-04 2차 지시).
/// </summary>
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;
}
/// <summary>유닛 선택(아군·적군 · null = 해제): 선택 링 + HUD 정보 + 사거리 표시(궁병은 도넛 · 범위 안 적 유무로 색).</summary>

View File

@ -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), "범위 안에 적이 생기면 도넛 붉게");

View File

@ -39,13 +39,14 @@ Unity.exe -projectPath <proj> -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). 커밋은 의도 변경 파일만.

View File

@ -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

View File

@ -25,7 +25,7 @@ Unity 6000.3.23f1 · 2D · 도형 플레이스홀더(아트 리소스는 별도
| 보병 | 첫 적까지 이동 후 공격(반격 받음) | 추가 이동 1회(이동만) |
| 기병 | 경로 위 모든 적 관통 피해 · 목표점까지 이동 | 제자리로 복귀 |
| 창병 | 보병과 동일 | 대기 — 적군 턴에 반경 안에서 움직이는 적에게 피해 |
| 궁병 | 목표 방향의 적을 향해 사거리까지 이동 후 사격(반격 없음) | 무조건 크리티컬 |
| 궁병 | 목표 방향 「슬링샷 거리 + 사거리」 안의 적을 향해 사거리까지 이동 후 사격(반격 없음) · 적이 없으면 목표점까지 이동 | 무조건 크리티컬 |
상성: 보병 > 창병 > 기병 > 보병. 궁병은 모두에게 약함(받는 피해 1.5배).