넓은 전장(20×32)·스크롤 카메라·미니맵·적 교전 범위 — PD 추가 지시 「스크롤 가능한 비교적 넓은 전장」 · 빈 곳 드래그 스크롤·휠 줌·행동 유닛 추적·미니맵 탭 이동 · 적은 교전 범위 안에서만 행동 · 스테이지 4종 전위/중군/후군 재배치 · 창병 대기 링 상시 표시 결함 수정 · 자동 플레이 timeScale 가속 · 테스트 14종

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-11-04 04:29:16 +09:00
parent 5135d828e2
commit ceaec991dd
18 changed files with 689 additions and 244 deletions

View File

@ -69,13 +69,16 @@ MonoBehaviour:
minDragDistance: 0.35 minDragDistance: 0.35
finalHitTolerance: 0.15 finalHitTolerance: 0.15
moveSpeed: 9 moveSpeed: 9
boardSize: {x: 12, y: 16} boardSize: {x: 20, y: 32}
cameraViewHeight: 18
cameraMinViewHeight: 10
cameraMaxViewHeight: 36
playerSpawnPoints: playerSpawnPoints:
- {x: -4, y: -5.5} - {x: -4, y: -13}
- {x: -2, y: -5.5} - {x: -2, y: -13}
- {x: 0, y: -5.5} - {x: 0, y: -13}
- {x: 2, y: -5.5} - {x: 2, y: -13}
- {x: 4, y: -5.5} - {x: 4, y: -13}
heroes: heroes:
- name: "\uC720\uBE44" - name: "\uC720\uBE44"
type: 0 type: 0
@ -101,184 +104,236 @@ MonoBehaviour:
- title: "\uD669\uAC74\uC801\uC758 \uB09C" - title: "\uD669\uAC74\uC801\uC758 \uB09C"
subtitle: "184\uB144" subtitle: "184\uB144"
aiAccuracy: 0.5 aiAccuracy: 0.5
aiAggroRange: 11
enemies: enemies:
- name: "\uD669\uAC74\uC801 \uBCF4\uBCD1" - name: "\uD669\uAC74\uC801 \uBCF4\uBCD1"
type: 0 type: 0
position: {x: -3.5, y: 2.5} position: {x: -3, y: -6}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uD669\uAC74\uC801 \uBCF4\uBCD1" - name: "\uD669\uAC74\uC801 \uBCF4\uBCD1"
type: 0 type: 0
position: {x: 3.5, y: 2.5} position: {x: 3, y: -6}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uD669\uAC74\uC801 \uCC3D\uBCD1" - name: "\uD669\uAC74\uC801 \uCC3D\uBCD1"
type: 3 type: 3
position: {x: 0, y: 2} position: {x: 0, y: -4.5}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uD669\uAC74\uC801 \uAD81\uBCD1" - name: "\uD669\uAC74\uC801 \uAD81\uBCD1"
type: 2 type: 2
position: {x: -2, y: 5} position: {x: -4, y: 1}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uD669\uAC74\uC801 \uAD81\uBCD1" - name: "\uD669\uAC74\uC801 \uAD81\uBCD1"
type: 2 type: 2
position: {x: 2, y: 5} position: {x: 4, y: 1}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uD669\uAC74\uC801 \uBCF4\uBCD1"
type: 0
position: {x: 0, y: 2.5}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uD669\uAC74\uC801 \uCC3D\uBCD1"
type: 3
position: {x: -2.5, y: 8}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC7A5\uAC01" - name: "\uC7A5\uAC01"
type: 0 type: 0
position: {x: 0, y: 6.5} position: {x: 0, y: 10}
hpMult: 1.3 hpMult: 1.3
atkMult: 1.2 atkMult: 1.2
isBoss: 1 isBoss: 1
- title: "\uD638\uB85C\uAD00 \uC804\uD22C" - title: "\uD638\uB85C\uAD00 \uC804\uD22C"
subtitle: "190\uB144" subtitle: "190\uB144"
aiAccuracy: 0.6 aiAccuracy: 0.6
aiAggroRange: 11
enemies: enemies:
- name: "\uB3D9\uD0C1\uAD70 \uCC3D\uBCD1" - name: "\uB3D9\uD0C1\uAD70 \uCC3D\uBCD1"
type: 3 type: 3
position: {x: -2, y: 2.2} position: {x: -2.5, y: -5}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uB3D9\uD0C1\uAD70 \uCC3D\uBCD1" - name: "\uB3D9\uD0C1\uAD70 \uCC3D\uBCD1"
type: 3 type: 3
position: {x: 2, y: 2.2} position: {x: 2.5, y: -5}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uB3D9\uD0C1\uAD70 \uAE30\uBCD1" - name: "\uB3D9\uD0C1\uAD70 \uAE30\uBCD1"
type: 1 type: 1
position: {x: -4, y: 3.5} position: {x: -6, y: -1}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uB3D9\uD0C1\uAD70 \uAE30\uBCD1" - name: "\uB3D9\uD0C1\uAD70 \uAE30\uBCD1"
type: 1 type: 1
position: {x: 4, y: 3.5} position: {x: 6, y: -1}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uB3D9\uD0C1\uAD70 \uBCF4\uBCD1"
type: 0
position: {x: 0, y: 1}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uB3D9\uD0C1\uAD70 \uAD81\uBCD1" - name: "\uB3D9\uD0C1\uAD70 \uAD81\uBCD1"
type: 2 type: 2
position: {x: -2.5, y: 5.5} position: {x: -3, y: 3}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uB3D9\uD0C1\uAD70 \uAD81\uBCD1" - name: "\uB3D9\uD0C1\uAD70 \uAD81\uBCD1"
type: 2 type: 2
position: {x: 2.5, y: 5.5} position: {x: 3, y: 3}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uB3D9\uD0C1\uAD70 \uAE30\uBCD1"
type: 1
position: {x: 0, y: 7}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC5EC\uD3EC" - name: "\uC5EC\uD3EC"
type: 1 type: 1
position: {x: 0, y: 4.5} position: {x: 0, y: 11}
hpMult: 1.6 hpMult: 1.6
atkMult: 1.5 atkMult: 1.5
isBoss: 1 isBoss: 1
- title: "\uC801\uBCBD\uB300\uC804" - title: "\uC801\uBCBD\uB300\uC804"
subtitle: "208\uB144" subtitle: "208\uB144"
aiAccuracy: 0.65 aiAccuracy: 0.65
aiAggroRange: 12
enemies: enemies:
- name: "\uC870\uC870\uAD70 \uBCF4\uBCD1" - name: "\uC870\uC870\uAD70 \uBCF4\uBCD1"
type: 0 type: 0
position: {x: -4, y: 2.5} position: {x: -5, y: -6}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC870\uC870\uAD70 \uBCF4\uBCD1" - name: "\uC870\uC870\uAD70 \uBCF4\uBCD1"
type: 0 type: 0
position: {x: 0, y: 2} position: {x: 0, y: -5}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC870\uC870\uAD70 \uBCF4\uBCD1" - name: "\uC870\uC870\uAD70 \uBCF4\uBCD1"
type: 0 type: 0
position: {x: 4, y: 2.5} position: {x: 5, y: -6}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC870\uC870\uAD70 \uAD81\uBCD1"
type: 2
position: {x: -2.5, y: 5}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC870\uC870\uAD70 \uAD81\uBCD1"
type: 2
position: {x: 2.5, y: 5}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC870\uC870\uAD70 \uAE30\uBCD1" - name: "\uC870\uC870\uAD70 \uAE30\uBCD1"
type: 1 type: 1
position: {x: 0, y: 4.2} position: {x: -3, y: 0}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC870\uC870\uAD70 \uAE30\uBCD1"
type: 1
position: {x: 3, y: 0}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC870\uC870\uAD70 \uAD81\uBCD1"
type: 2
position: {x: -4, y: 5}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC870\uC870\uAD70 \uAD81\uBCD1"
type: 2
position: {x: 4, y: 5}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC870\uC870\uAD70 \uCC3D\uBCD1"
type: 3
position: {x: 0, y: 7}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC870\uC870" - name: "\uC870\uC870"
type: 0 type: 0
position: {x: 0, y: 6.8} position: {x: 0, y: 12}
hpMult: 1.5 hpMult: 1.5
atkMult: 1.3 atkMult: 1.3
isBoss: 1 isBoss: 1
- title: "\uC624\uC7A5\uC6D0 \uC804\uD22C" - title: "\uC624\uC7A5\uC6D0 \uC804\uD22C"
subtitle: "234\uB144" subtitle: "234\uB144"
aiAccuracy: 0.7 aiAccuracy: 0.7
aiAggroRange: 12
enemies: enemies:
- name: "\uC704\uAD70 \uCC3D\uBCD1" - name: "\uC704\uAD70 \uCC3D\uBCD1"
type: 3 type: 3
position: {x: -2.5, y: 2.2} position: {x: -3, y: -6}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC704\uAD70 \uCC3D\uBCD1" - name: "\uC704\uAD70 \uCC3D\uBCD1"
type: 3 type: 3
position: {x: 2.5, y: 2.2} position: {x: 3, y: -6}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC704\uAD70 \uBCF4\uBCD1" - name: "\uC704\uAD70 \uBCF4\uBCD1"
type: 0 type: 0
position: {x: -4.5, y: 3.5} position: {x: -7, y: -2}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC704\uAD70 \uBCF4\uBCD1" - name: "\uC704\uAD70 \uBCF4\uBCD1"
type: 0 type: 0
position: {x: 4.5, y: 3.5} position: {x: 7, y: -2}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC704\uAD70 \uAE30\uBCD1" - name: "\uC704\uAD70 \uAE30\uBCD1"
type: 1 type: 1
position: {x: -1.5, y: 4.5} position: {x: -2, y: 2}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC704\uAD70 \uAE30\uBCD1" - name: "\uC704\uAD70 \uAE30\uBCD1"
type: 1 type: 1
position: {x: 1.5, y: 4.5} position: {x: 2, y: 2}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC704\uAD70 \uAD81\uBCD1" - name: "\uC704\uAD70 \uAD81\uBCD1"
type: 2 type: 2
position: {x: 0, y: 6} position: {x: -4, y: 7}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC704\uAD70 \uAD81\uBCD1"
type: 2
position: {x: 4, y: 7}
hpMult: 1
atkMult: 1
isBoss: 0
- name: "\uC704\uAD70 \uCC3D\uBCD1"
type: 3
position: {x: 0, y: 9}
hpMult: 1 hpMult: 1
atkMult: 1 atkMult: 1
isBoss: 0 isBoss: 0
- name: "\uC0AC\uB9C8\uC758" - name: "\uC0AC\uB9C8\uC758"
type: 0 type: 0
position: {x: 0, y: 7.2} position: {x: 0, y: 13}
hpMult: 1.6 hpMult: 1.6
atkMult: 1.3 atkMult: 1.3
isBoss: 1 isBoss: 1

View File

@ -24,9 +24,9 @@ RenderSettings:
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1 m_AmbientIntensity: 1
m_AmbientMode: 0 m_AmbientMode: 3
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5 m_HaloStrength: 0.5
m_FlareStrength: 1 m_FlareStrength: 1
m_FlareFadeSpeed: 3 m_FlareFadeSpeed: 3
@ -50,7 +50,7 @@ LightmapSettings:
m_IndirectOutputScale: 1 m_IndirectOutputScale: 1
m_AlbedoBoost: 1 m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0 m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1 m_EnableBakedLightmaps: 0
m_EnableRealtimeLightmaps: 0 m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings: m_LightmapEditorSettings:
serializedVersion: 12 serializedVersion: 12
@ -119,7 +119,7 @@ NavMeshSettings:
debug: debug:
m_Flags: 0 m_Flags: 0
m_NavMeshData: {fileID: 0} m_NavMeshData: {fileID: 0}
--- !u!1 &930732900 --- !u!1 &1288297926
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
@ -127,9 +127,56 @@ GameObject:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
serializedVersion: 6 serializedVersion: 6
m_Component: m_Component:
- component: {fileID: 930732903} - component: {fileID: 1288297928}
- component: {fileID: 930732902} - component: {fileID: 1288297927}
- component: {fileID: 930732901} m_Layer: 0
m_Name: GameBootstrap
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1288297927
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1288297926}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0d49dc40532d984485290e1cfc0f91f, type: 3}
m_Name:
m_EditorClassIdentifier: TKS.Runtime::TKS.GameBootstrap
Config: {fileID: 11400000, guid: 6b41999f2857842408b1a9edeaf47cb8, type: 2}
AutoStart: 1
StartStageIndex: 0
--- !u!4 &1288297928
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1288297926}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1938987432
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1938987435}
- component: {fileID: 1938987434}
- component: {fileID: 1938987433}
m_Layer: 0 m_Layer: 0
m_Name: Main Camera m_Name: Main Camera
m_TagString: MainCamera m_TagString: MainCamera
@ -137,21 +184,21 @@ GameObject:
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 1 m_IsActive: 1
--- !u!81 &930732901 --- !u!81 &1938987433
AudioListener: AudioListener:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 930732900} m_GameObject: {fileID: 1938987432}
m_Enabled: 1 m_Enabled: 1
--- !u!20 &930732902 --- !u!20 &1938987434
Camera: Camera:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 930732900} m_GameObject: {fileID: 1938987432}
m_Enabled: 1 m_Enabled: 1
serializedVersion: 2 serializedVersion: 2
m_ClearFlags: 2 m_ClearFlags: 2
@ -196,13 +243,13 @@ Camera:
m_OcclusionCulling: 1 m_OcclusionCulling: 1
m_StereoConvergence: 10 m_StereoConvergence: 10
m_StereoSeparation: 0.022 m_StereoSeparation: 0.022
--- !u!4 &930732903 --- !u!4 &1938987435
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0} m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0} m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 930732900} m_GameObject: {fileID: 1938987432}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: -10} m_LocalPosition: {x: 0, y: 0, z: -10}
@ -211,56 +258,9 @@ Transform:
m_Children: [] m_Children: []
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1640921812
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1640921814}
- component: {fileID: 1640921813}
m_Layer: 0
m_Name: GameBootstrap
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1640921813
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1640921812}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0d49dc40532d984485290e1cfc0f91f, type: 3}
m_Name:
m_EditorClassIdentifier: TKS.Runtime::TKS.GameBootstrap
Config: {fileID: 11400000, guid: 6b41999f2857842408b1a9edeaf47cb8, type: 2}
AutoStart: 1
StartStageIndex: 0
--- !u!4 &1640921814
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1640921812}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1660057539 &9223372036854775807 --- !u!1660057539 &9223372036854775807
SceneRoots: SceneRoots:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
m_Roots: m_Roots:
- {fileID: 930732903} - {fileID: 1938987435}
- {fileID: 1640921814} - {fileID: 1288297928}

View File

@ -5,7 +5,7 @@ namespace TKS.EditorTools
{ {
/// <summary> /// <summary>
/// BalanceConfig 자산의 최초 1회 시드 값(프로토타입 초안). 이후 수치 조정은 자산(Inspector)에서 한다 — 코드가 SOT 가 아니다. /// BalanceConfig 자산의 최초 1회 시드 값(프로토타입 초안). 이후 수치 조정은 자산(Inspector)에서 한다 — 코드가 SOT 가 아니다.
/// 전장 = 가로 12 × 세로 16 (x −6~6 · y −8~8). 아군은 아래쪽(y −5.5), 적은 위쪽 절반. /// 전장 = 가로 20 × 세로 32 (x −10~10 · y −16~16) · 화면에는 세로 18 만 보여 스크롤한다. 아군은 아래(y −13) · 적은 전위/중군/후군으로 흩어 배치.
/// </summary> /// </summary>
public static class BalanceDefaults public static class BalanceDefaults
{ {
@ -44,10 +44,13 @@ namespace TKS.EditorTools
c.finalHitTolerance = 0.15f; c.finalHitTolerance = 0.15f;
c.moveSpeed = 9f; c.moveSpeed = 9f;
c.boardSize = new Vector2(12f, 16f); c.boardSize = new Vector2(20f, 32f);
c.cameraViewHeight = 18f;
c.cameraMinViewHeight = 10f;
c.cameraMaxViewHeight = 36f;
c.playerSpawnPoints = new[] c.playerSpawnPoints = new[]
{ {
new Vector2(-4f, -5.5f), new Vector2(-2f, -5.5f), new Vector2(0f, -5.5f), new Vector2(2f, -5.5f), new Vector2(4f, -5.5f), new Vector2(-4f, -13f), new Vector2(-2f, -13f), new Vector2(0f, -13f), new Vector2(2f, -13f), new Vector2(4f, -13f),
}; };
c.heroes = new[] c.heroes = new[]
@ -61,38 +64,49 @@ namespace TKS.EditorTools
c.stages = new[] c.stages = new[]
{ {
Stage("황건적의 난", "184년", 0.5f, Stage("황건적의 난", "184년", 0.5f, 11f,
E("황건적 보병", UnitType.Infantry, -3.5f, 2.5f), // 전위
E("황건적 보병", UnitType.Infantry, 3.5f, 2.5f), E("황건적 보병", UnitType.Infantry, -3f, -6f),
E("황건적 창병", UnitType.Spear, 0f, 2.0f), E("황건적 보병", UnitType.Infantry, 3f, -6f),
E("황건적 궁병", UnitType.Archer, -2f, 5f), E("황건적 창병", UnitType.Spear, 0f, -4.5f),
E("황건적 궁병", UnitType.Archer, 2f, 5f), // 중군
Boss("장각", UnitType.Infantry, 0f, 6.5f, 1.3f, 1.2f)), E("황건적 궁병", UnitType.Archer, -4f, 1f),
Stage("호로관 전투", "190년", 0.6f, E("황건적 궁병", UnitType.Archer, 4f, 1f),
E("동탁군 창병", UnitType.Spear, -2f, 2.2f), E("황건적 보병", UnitType.Infantry, 0f, 2.5f),
E("동탁군 창병", UnitType.Spear, 2f, 2.2f), // 후군
E("동탁군 기병", UnitType.Cavalry, -4f, 3.5f), E("황건적 창병", UnitType.Spear, -2.5f, 8f),
E("동탁군 기병", UnitType.Cavalry, 4f, 3.5f), Boss("장각", UnitType.Infantry, 0f, 10f, 1.3f, 1.2f)),
E("동탁군 궁병", UnitType.Archer, -2.5f, 5.5f), Stage("호로관 전투", "190년", 0.6f, 11f,
E("동탁군 궁병", UnitType.Archer, 2.5f, 5.5f), E("동탁군 창병", UnitType.Spear, -2.5f, -5f),
Boss("여포", UnitType.Cavalry, 0f, 4.5f, 1.6f, 1.5f)), E("동탁군 창병", UnitType.Spear, 2.5f, -5f),
Stage("적벽대전", "208년", 0.65f, E("동탁군 기병", UnitType.Cavalry, -6f, -1f),
E("조조군 보병", UnitType.Infantry, -4f, 2.5f), E("동탁군 기병", UnitType.Cavalry, 6f, -1f),
E("조조군 보병", UnitType.Infantry, 0f, 2.0f), E("동탁군 보병", UnitType.Infantry, 0f, 1f),
E("조조군 보병", UnitType.Infantry, 4f, 2.5f), E("동탁군 궁병", UnitType.Archer, -3f, 3f),
E("조조군 궁병", UnitType.Archer, -2.5f, 5f), E("동탁군 궁병", UnitType.Archer, 3f, 3f),
E("조조군 궁병", UnitType.Archer, 2.5f, 5f), E("동탁군 기병", UnitType.Cavalry, 0f, 7f),
E("조조군 기병", UnitType.Cavalry, 0f, 4.2f), Boss("여포", UnitType.Cavalry, 0f, 11f, 1.6f, 1.5f)),
Boss("조조", UnitType.Infantry, 0f, 6.8f, 1.5f, 1.3f)), Stage("적벽대전", "208년", 0.65f, 12f,
Stage("오장원 전투", "234년", 0.7f, E("조조군 보병", UnitType.Infantry, -5f, -6f),
E("위군 창병", UnitType.Spear, -2.5f, 2.2f), E("조조군 보병", UnitType.Infantry, 0f, -5f),
E("위군 창병", UnitType.Spear, 2.5f, 2.2f), E("조조군 보병", UnitType.Infantry, 5f, -6f),
E("위군 보병", UnitType.Infantry, -4.5f, 3.5f), E("조조군 기병", UnitType.Cavalry, -3f, 0f),
E("위군 보병", UnitType.Infantry, 4.5f, 3.5f), E("조조군 기병", UnitType.Cavalry, 3f, 0f),
E("위군 기병", UnitType.Cavalry, -1.5f, 4.5f), E("조조군 궁병", UnitType.Archer, -4f, 5f),
E("위군 기병", UnitType.Cavalry, 1.5f, 4.5f), E("조조군 궁병", UnitType.Archer, 4f, 5f),
E("위군 궁병", UnitType.Archer, 0f, 6f), E("조조군 창병", UnitType.Spear, 0f, 7f),
Boss("사마의", UnitType.Infantry, 0f, 7.2f, 1.6f, 1.3f)), Boss("조조", UnitType.Infantry, 0f, 12f, 1.5f, 1.3f)),
Stage("오장원 전투", "234년", 0.7f, 12f,
E("위군 창병", UnitType.Spear, -3f, -6f),
E("위군 창병", UnitType.Spear, 3f, -6f),
E("위군 보병", UnitType.Infantry, -7f, -2f),
E("위군 보병", UnitType.Infantry, 7f, -2f),
E("위군 기병", UnitType.Cavalry, -2f, 2f),
E("위군 기병", UnitType.Cavalry, 2f, 2f),
E("위군 궁병", UnitType.Archer, -4f, 7f),
E("위군 궁병", UnitType.Archer, 4f, 7f),
E("위군 창병", UnitType.Spear, 0f, 9f),
Boss("사마의", UnitType.Infantry, 0f, 13f, 1.6f, 1.3f)),
}; };
} }
@ -116,9 +130,9 @@ namespace TKS.EditorTools
return new BalanceConfig.EnemySpawn { name = name, type = t, position = new Vector2(x, y), hpMult = hpMult, atkMult = atkMult, isBoss = true }; return new BalanceConfig.EnemySpawn { name = name, type = t, position = new Vector2(x, y), hpMult = hpMult, atkMult = atkMult, isBoss = true };
} }
static BalanceConfig.StageDef Stage(string title, string subtitle, float accuracy, params BalanceConfig.EnemySpawn[] enemies) static BalanceConfig.StageDef Stage(string title, string subtitle, float accuracy, float aggro, params BalanceConfig.EnemySpawn[] enemies)
{ {
return new BalanceConfig.StageDef { title = title, subtitle = subtitle, aiAccuracy = accuracy, enemies = new List<BalanceConfig.EnemySpawn>(enemies) }; return new BalanceConfig.StageDef { title = title, subtitle = subtitle, aiAccuracy = accuracy, aiAggroRange = aggro, enemies = new List<BalanceConfig.EnemySpawn>(enemies) };
} }
} }
} }

View File

@ -33,6 +33,17 @@ namespace TKS.EditorTools
Debug.Log("[TKS] BalanceConfig 기본값 재시드"); Debug.Log("[TKS] BalanceConfig 기본값 재시드");
} }
/// <summary>CLI 용: 기본값 재시드 + 씬·설정 재생성(기존 자산 수치를 덮어쓴다).</summary>
public static void BuildFresh()
{
var cfg = EnsureConfig(true);
BuildScene(cfg);
ApplySettings();
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[TKS] BuildFresh 완료 · 기본값 재시드 · " + ScenePath);
}
public static BalanceConfig EnsureConfig(bool overwrite) public static BalanceConfig EnsureConfig(bool overwrite)
{ {
if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources"); if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources");
@ -81,6 +92,7 @@ namespace TKS.EditorTools
PlayerSettings.companyName = "NerdNavis"; PlayerSettings.companyName = "NerdNavis";
PlayerSettings.productName = "ThreeKingdomsSlingshot"; PlayerSettings.productName = "ThreeKingdomsSlingshot";
PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait; PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait;
PlayerSettings.runInBackground = true;
EditorSettings.defaultBehaviorMode = EditorBehaviorMode.Mode2D; EditorSettings.defaultBehaviorMode = EditorBehaviorMode.Mode2D;
} }

View File

@ -8,6 +8,7 @@ namespace TKS
/// <summary> /// <summary>
/// 자동 플레이 실측: 아군도 AI 로 조작해 몇 턴을 돌리며 스크린샷·로그를 남긴다. /// 자동 플레이 실측: 아군도 AI 로 조작해 몇 턴을 돌리며 스크린샷·로그를 남긴다.
/// 에디터에서 ProjectSetup.AutoPlayCapture 가 SessionState 로 출력 폴더를 넘기면 생성된다. /// 에디터에서 ProjectSetup.AutoPlayCapture 가 SessionState 로 출력 폴더를 넘기면 생성된다.
/// 비활성 에디터 창은 프레임이 느리므로 timeScale 을 올려 실시간 기준으로 빨리 끝낸다.
/// </summary> /// </summary>
public class AutoPlayDriver : MonoBehaviour public class AutoPlayDriver : MonoBehaviour
{ {
@ -36,6 +37,8 @@ namespace TKS
IEnumerator Start() IEnumerator Start()
{ {
Directory.CreateDirectory(OutputDir); Directory.CreateDirectory(OutputDir);
Time.timeScale = 4f;
Time.maximumDeltaTime = 1f;
float t0 = Time.realtimeSinceStartup; float t0 = Time.realtimeSinceStartup;
while ((BattleManager.Instance == null || BattleManager.Instance.Phase != BattlePhase.PlayerTurn) while ((BattleManager.Instance == null || BattleManager.Instance.Phase != BattlePhase.PlayerTurn)
&& Time.realtimeSinceStartup - t0 < 20f) && Time.realtimeSinceStartup - t0 < 20f)
@ -50,22 +53,42 @@ namespace TKS
Finish(1); Finish(1);
yield break; yield break;
} }
var camCtrl = cam.GetComponent<CameraController>();
Log("stage=" + bm.Stage.title + " players=" + bm.AliveCount(Faction.Player) + " enemies=" + bm.AliveCount(Faction.Enemy)); Log("stage=" + bm.Stage.title + " players=" + bm.AliveCount(Faction.Player) + " enemies=" + bm.AliveCount(Faction.Enemy) + " board=" + bm.Config.boardSize);
yield return Fx.Wait(0.6f); yield return Fx.Wait(0.6f);
yield return Shot(cam, "start"); yield return Shot(cam, "start");
// 전장 전체 조감(최대 줌아웃) → 원래 줌
if (camCtrl != null)
{
float prev = camCtrl.ViewHeight;
camCtrl.SetViewHeight(camCtrl.MaxViewHeight);
camCtrl.SnapTo(Vector2.zero);
yield return null;
yield return Shot(cam, "overview_zoom_out");
camCtrl.SetViewHeight(prev);
camCtrl.PanBy(new Vector2(0f, 6f)); // 스크롤 확인
yield return null;
yield return Shot(cam, "scrolled_up");
camCtrl.FocusOn(bm.AliveUnits(Faction.Player)[0].Position + new Vector2(0f, 3f));
yield return Fx.Wait(0.5f);
}
var first = FirstActable(bm); var first = FirstActable(bm);
if (first != null) if (first != null)
{ {
var tgt = EnemyAI.Decide(first, bm.AliveUnits(Faction.Enemy), 1f, bm.Rng, bm.Board); var tgt = EnemyAI.Decide(first, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board);
Vector2 pointer = first.Position - (tgt - first.Position) * 0.5f; if (tgt.HasValue)
bm.Sling.ShowPreview(first, tgt, pointer); {
first.View.SetSelected(true); Vector2 pointer = first.Position - (tgt.Value - first.Position) * 0.5f;
yield return null; bm.Sling.ShowPreview(first, tgt.Value, pointer);
yield return Shot(cam, "aim_" + first.DisplayName); first.View.SetSelected(true);
first.View.SetSelected(false); yield return null;
bm.Sling.HidePreview(); yield return Shot(cam, "aim_" + first.DisplayName);
first.View.SetSelected(false);
bm.Sling.HidePreview();
}
} }
bool firstActionShot = false; bool firstActionShot = false;
@ -80,8 +103,9 @@ namespace TKS
bm.OnEmptyTap(); bm.OnEmptyTap();
break; break;
} }
var target = EnemyAI.Decide(u, bm.AliveUnits(Faction.Enemy), 1f, bm.Rng, bm.Board); var target = EnemyAI.Decide(u, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board);
if (!bm.RequestPlayerAction(u, target)) break; if (!target.HasValue) break;
if (!bm.RequestPlayerAction(u, target.Value)) break;
while (bm.Phase == BattlePhase.Resolving) yield return null; while (bm.Phase == BattlePhase.Resolving) yield return null;
Log("turn " + turn + " | " + bm.LastMessage); Log("turn " + turn + " | " + bm.LastMessage);
if (!firstActionShot) if (!firstActionShot)
@ -93,11 +117,11 @@ namespace TKS
} }
while (bm.Phase == BattlePhase.EnemyTurn || bm.Phase == BattlePhase.Resolving) yield return null; while (bm.Phase == BattlePhase.EnemyTurn || bm.Phase == BattlePhase.Resolving) yield return null;
Log("turn " + turn + " end | phase=" + bm.Phase + " players=" + bm.AliveCount(Faction.Player) + " enemies=" + bm.AliveCount(Faction.Enemy) + " | " + bm.LastMessage); Log("turn " + turn + " end | phase=" + bm.Phase + " players=" + bm.AliveCount(Faction.Player) + " enemies=" + bm.AliveCount(Faction.Enemy) + " | " + bm.LastMessage);
yield return Fx.Wait(0.4f);
yield return Shot(cam, "turn" + turn + "_end"); yield return Shot(cam, "turn" + turn + "_end");
if (bm.Phase == BattlePhase.StageClear || bm.Phase == BattlePhase.Defeat) break; if (bm.Phase == BattlePhase.StageClear || bm.Phase == BattlePhase.Defeat) break;
} }
File.WriteAllLines(Path.Combine(OutputDir, "autoplay_log.txt"), log.ToArray());
Finish(0); Finish(0);
} }
@ -122,10 +146,6 @@ namespace TKS
{ {
log.Add(s); log.Add(s);
Debug.Log("[AutoPlay] " + s); Debug.Log("[AutoPlay] " + s);
}
void Finish(int code)
{
try try
{ {
File.WriteAllLines(Path.Combine(OutputDir, "autoplay_log.txt"), log.ToArray()); File.WriteAllLines(Path.Combine(OutputDir, "autoplay_log.txt"), log.ToArray());
@ -133,6 +153,12 @@ namespace TKS
catch catch
{ {
} }
}
void Finish(int code)
{
Log("finish code=" + code);
Time.timeScale = 1f;
#if UNITY_EDITOR #if UNITY_EDITOR
UnityEditor.EditorApplication.Exit(code); UnityEditor.EditorApplication.Exit(code);
#else #else

View File

@ -51,6 +51,8 @@ namespace TKS
public string title; public string title;
public string subtitle; public string subtitle;
[Range(0f, 1f)] public float aiAccuracy = 0.6f; [Range(0f, 1f)] public float aiAccuracy = 0.6f;
[Tooltip("이 거리 안에 아군이 들어와야 적이 움직인다(넓은 전장에서 부대별 교전)")]
public float aiAggroRange = 11f;
public List<EnemySpawn> enemies = new List<EnemySpawn>(); public List<EnemySpawn> enemies = new List<EnemySpawn>();
} }
@ -82,8 +84,12 @@ namespace TKS
public float finalHitTolerance = 0.15f; public float finalHitTolerance = 0.15f;
public float moveSpeed = 9f; public float moveSpeed = 9f;
[Header("전장")] [Header("전장·카메라 (화면보다 넓은 전장을 스크롤)")]
public Vector2 boardSize = new Vector2(12f, 16f); public Vector2 boardSize = new Vector2(20f, 32f);
[Tooltip("화면 세로에 보이는 월드 높이")]
public float cameraViewHeight = 18f;
public float cameraMinViewHeight = 10f;
public float cameraMaxViewHeight = 36f;
public Vector2[] playerSpawnPoints = new Vector2[0]; public Vector2[] playerSpawnPoints = new Vector2[0];
[Header("아군 영웅 (최대 5)")] [Header("아군 영웅 (최대 5)")]

View File

@ -1,13 +1,16 @@
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
using UnityEngine.UI; using UnityEngine.UI;
namespace TKS namespace TKS
{ {
/// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·스킬 아이콘(추후)·결과 오버레이.</summary> /// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·미니맵·스킬 아이콘(추후)·결과 오버레이.</summary>
public class BattleHUD : MonoBehaviour public class BattleHUD : MonoBehaviour
{ {
BattleManager battle; BattleManager battle;
Camera uiCam;
CameraController camCtrl;
Text stageText; Text stageText;
Text turnText; Text turnText;
Text countText; Text countText;
@ -19,9 +22,15 @@ namespace TKS
Text resultSub; Text resultSub;
Unit selected; Unit selected;
public void Build(BattleManager b, Camera cam) Image minimapBg;
Image minimapView;
readonly Dictionary<Unit, Image> minimapDots = new Dictionary<Unit, Image>();
public void Build(BattleManager b, Camera cam, CameraController cc)
{ {
battle = b; battle = b;
uiCam = cam;
camCtrl = cc;
var canvas = gameObject.AddComponent<Canvas>(); var canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceCamera; canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = cam; canvas.worldCamera = cam;
@ -58,6 +67,16 @@ namespace TKS
skillLabel.text = "스킬\n(추후)"; skillLabel.text = "스킬\n(추후)";
skillLabel.color = new Color(0.75f, 0.75f, 0.75f); skillLabel.color = new Color(0.75f, 0.75f, 0.75f);
// 미니맵 (전장 비율 · 탭하면 그 지점으로 카메라 이동)
Vector2 bs = battle.Config.boardSize;
float mmW = 180f;
float mmH = mmW * bs.y / Mathf.Max(0.01f, bs.x);
minimapBg = MakeImage("Minimap", transform, new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(1f, 1f), new Vector2(-30f, -215f), new Vector2(mmW, mmH), new Color(0.06f, 0.1f, 0.06f, 0.8f));
var mmBorder = MakeImage("Border", minimapBg.transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(6f, 6f), new Color(0.85f, 0.75f, 0.45f, 0.9f));
mmBorder.transform.SetAsFirstSibling();
var mmMid = MakeImage("Mid", minimapBg.transform, new Vector2(0f, 0.5f), new Vector2(1f, 0.5f), new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(0f, 2f), new Color(1f, 1f, 1f, 0.25f));
minimapView = MakeImage("View", minimapBg.transform, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero, new Vector2(40f, 60f), new Color(1f, 1f, 1f, 0.2f));
resultOverlay = MakeImage("ResultOverlay", transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, 0.65f)).gameObject; resultOverlay = MakeImage("ResultOverlay", transform, Vector2.zero, Vector2.one, new Vector2(0.5f, 0.5f), Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, 0.65f)).gameObject;
resultTitle = MakeText("Title", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 60f), new Vector2(1000f, 160f), 110, TextAnchor.MiddleCenter); resultTitle = MakeText("Title", new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0f, 60f), new Vector2(1000f, 160f), 110, TextAnchor.MiddleCenter);
resultTitle.transform.SetParent(resultOverlay.transform, false); resultTitle.transform.SetParent(resultOverlay.transform, false);
@ -111,10 +130,28 @@ namespace TKS
selected = u; selected = u;
} }
/// <summary>화면 점이 미니맵 안이면 해당 월드 좌표를 돌려준다.</summary>
public bool TryMinimapTap(Vector2 screen, out Vector2 world)
{
world = Vector2.zero;
if (minimapBg == null || battle == null || battle.Board == null) return false;
var rt = minimapBg.rectTransform;
if (!RectTransformUtility.RectangleContainsScreenPoint(rt, screen, uiCam)) return false;
Vector2 local;
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, screen, uiCam, out local)) return false;
var r = rt.rect;
float nx = Mathf.Clamp01((local.x - r.xMin) / r.width);
float ny = Mathf.Clamp01((local.y - r.yMin) / r.height);
var b = battle.Board.Rect;
world = new Vector2(b.xMin + nx * b.width, b.yMin + ny * b.height);
return true;
}
void LateUpdate() void LateUpdate()
{ {
if (battle == null || battle.Config == null || battle.Config.stages.Length == 0) return; if (battle == null || battle.Config == null || battle.Config.stages.Length == 0) return;
Refresh(); Refresh();
RefreshMinimap();
} }
void Refresh() void Refresh()
@ -138,7 +175,7 @@ namespace TKS
{ {
hintText.text = battle.OnlyBonusMovesRemain() hintText.text = battle.OnlyBonusMovesRemain()
? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀" ? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀"
: "아군 유닛을 잡고 뒤로 당겼다 놓으면 반대 방향으로 이동·공격"; : "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 빈 곳 드래그 = 전장 이동";
} }
else else
{ {
@ -178,5 +215,55 @@ namespace TKS
: stage.title + " 에서 패배\n화면을 탭하면 다시 도전"; : stage.title + " 에서 패배\n화면을 탭하면 다시 도전";
} }
} }
void RefreshMinimap()
{
if (minimapBg == null || battle.Board == null) return;
var r = minimapBg.rectTransform.rect;
var b = battle.Board.Rect;
float sx = r.width / b.width;
float sy = r.height / b.height;
// 카메라 시야 사각형
if (camCtrl != null)
{
var v = camCtrl.ViewRect;
float x0 = Mathf.Clamp((v.xMin - b.xMin) * sx, 0f, r.width);
float y0 = Mathf.Clamp((v.yMin - b.yMin) * sy, 0f, r.height);
float x1 = Mathf.Clamp((v.xMax - b.xMin) * sx, 0f, r.width);
float y1 = Mathf.Clamp((v.yMax - b.yMin) * sy, 0f, r.height);
minimapView.rectTransform.anchoredPosition = new Vector2(x0, y0);
minimapView.rectTransform.sizeDelta = new Vector2(Mathf.Max(2f, x1 - x0), Mathf.Max(2f, y1 - y0));
}
// 유닛 점
foreach (var u in battle.Units)
{
if (u == null) continue;
Image dot;
if (!minimapDots.TryGetValue(u, out dot))
{
dot = MakeImage("Dot", minimapBg.transform, Vector2.zero, Vector2.zero, new Vector2(0.5f, 0.5f), Vector2.zero,
new Vector2(u.IsBoss ? 14f : 10f, u.IsBoss ? 14f : 10f),
u.Faction == Faction.Player ? UnitView.PlayerColor : UnitView.EnemyColor);
if (u.IsBoss) dot.color = new Color(1f, 0.85f, 0.3f);
minimapDots[u] = dot;
}
bool show = u.IsAlive;
if (dot.gameObject.activeSelf != show) dot.gameObject.SetActive(show);
if (show) dot.rectTransform.anchoredPosition = new Vector2((u.Position.x - b.xMin) * sx, (u.Position.y - b.yMin) * sy);
}
// 사라진 유닛 정리
var dead = new List<Unit>();
foreach (var kv in minimapDots)
{
if (kv.Key == null) dead.Add(kv.Key);
}
foreach (var k in dead)
{
if (minimapDots[k] != null) Destroy(minimapDots[k].gameObject);
minimapDots.Remove(k);
}
}
} }
} }

View File

@ -44,7 +44,14 @@ namespace TKS
get { return Config.stages[Mathf.Clamp(StageIndex, 0, Config.stages.Length - 1)]; } get { return Config.stages[Mathf.Clamp(StageIndex, 0, Config.stages.Length - 1)]; }
} }
/// <summary>상태가 바뀔 때(HUD 갱신용).</summary>
public event Action Changed; public event Action Changed;
/// <summary>단계가 바뀔 때(카메라 연동용).</summary>
public event Action<BattlePhase> PhaseChanged;
/// <summary>어떤 유닛이 행동을 시작할 때(카메라 추적용).</summary>
public event Action<Unit> UnitActing;
/// <summary>스테이지 배치가 끝났을 때.</summary>
public event Action StageStarted;
void Awake() void Awake()
{ {
@ -72,6 +79,14 @@ namespace TKS
if (Changed != null) Changed(); if (Changed != null) Changed();
} }
void SetPhase(BattlePhase p)
{
if (Phase == p) return;
Phase = p;
if (PhaseChanged != null) PhaseChanged(p);
NotifyChanged();
}
// ---------- 유닛 ---------- // ---------- 유닛 ----------
public List<Unit> AliveUnits(Faction f) public List<Unit> AliveUnits(Faction f)
@ -129,6 +144,7 @@ namespace TKS
{ {
StopAllCoroutines(); StopAllCoroutines();
ClearUnits(); ClearUnits();
Phase = BattlePhase.Setup;
StageIndex = Mathf.Clamp(index, 0, Config.stages.Length - 1); StageIndex = Mathf.Clamp(index, 0, Config.stages.Length - 1);
var stage = Config.stages[StageIndex]; var stage = Config.stages[StageIndex];
@ -144,9 +160,9 @@ namespace TKS
} }
TurnNumber = 1; TurnNumber = 1;
Phase = BattlePhase.PlayerTurn;
LastMessage = stage.title + " — 아군 차례"; LastMessage = stage.title + " — 아군 차례";
NotifyChanged(); if (StageStarted != null) StageStarted();
SetPhase(BattlePhase.PlayerTurn);
} }
public void ContinueAfterResult() public void ContinueAfterResult()
@ -171,8 +187,8 @@ namespace TKS
public bool RequestPlayerAction(Unit u, Vector2 target) public bool RequestPlayerAction(Unit u, Vector2 target)
{ {
if (!CanAct(u)) return false; if (!CanAct(u)) return false;
Phase = BattlePhase.Resolving; SetPhase(BattlePhase.Resolving);
NotifyChanged(); if (UnitActing != null) UnitActing(u);
StartCoroutine(PlayerActionRoutine(u, target)); StartCoroutine(PlayerActionRoutine(u, target));
return true; return true;
} }
@ -196,8 +212,7 @@ namespace TKS
} }
else else
{ {
Phase = BattlePhase.PlayerTurn; SetPhase(BattlePhase.PlayerTurn);
NotifyChanged();
} }
} }
@ -231,8 +246,7 @@ namespace TKS
{ {
if (u.BonusMoveAvailable) u.MarkActed(); if (u.BonusMoveAvailable) u.MarkActed();
} }
Phase = BattlePhase.Resolving; SetPhase(BattlePhase.Resolving);
NotifyChanged();
StartCoroutine(EnemyTurnRoutine()); StartCoroutine(EnemyTurnRoutine());
} }
@ -245,14 +259,16 @@ namespace TKS
IEnumerator EnemyTurnRoutine() IEnumerator EnemyTurnRoutine()
{ {
Phase = BattlePhase.EnemyTurn; SetPhase(BattlePhase.EnemyTurn);
LastMessage = "적군 차례"; LastMessage = "적군 차례";
NotifyChanged(); NotifyChanged();
foreach (var e in AliveUnits(Faction.Enemy)) e.ResetForNewTurn(); foreach (var e in AliveUnits(Faction.Enemy)) e.ResetForNewTurn();
yield return Fx.Wait(0.5f); yield return Fx.Wait(0.4f);
var enemies = AliveUnits(Faction.Enemy); var enemies = AliveUnits(Faction.Enemy);
float accuracy = Stage.aiAccuracy; float accuracy = Stage.aiAccuracy;
float aggro = Stage.aiAggroRange;
int acted = 0;
for (int i = 0; i < enemies.Count; i++) for (int i = 0; i < enemies.Count; i++)
{ {
var e = enemies[i]; var e = enemies[i];
@ -260,21 +276,26 @@ namespace TKS
var players = AliveUnits(Faction.Player); var players = AliveUnits(Faction.Player);
if (players.Count == 0) break; if (players.Count == 0) break;
Vector2 target = EnemyAI.Decide(e, players, accuracy, Rng, Board); Vector2? target = EnemyAI.Decide(e, players, accuracy, aggro, Rng, Board);
if (!target.HasValue) continue; // 교전 범위 밖 → 자리 지킴
if (UnitActing != null) UnitActing(e);
yield return Fx.Wait(0.3f); // 카메라가 따라올 시간
var o = new ActionOutcome(); var o = new ActionOutcome();
yield return Resolver.Resolve(e, target, false, o); yield return Resolver.Resolve(e, target.Value, false, o);
yield return ApplyOverwatch(e, o.Path); yield return ApplyOverwatch(e, o.Path);
LastMessage = Describe(e, o, false); LastMessage = Describe(e, o, false);
NotifyChanged(); NotifyChanged();
acted++;
if (CheckBattleEnd()) yield break; if (CheckBattleEnd()) yield break;
yield return Fx.Wait(0.25f); yield return Fx.Wait(0.25f);
} }
if (acted == 0) yield return Fx.Wait(0.3f);
foreach (var u in AliveUnits(Faction.Player)) u.ResetForNewTurn(); foreach (var u in AliveUnits(Faction.Player)) u.ResetForNewTurn();
TurnNumber++; TurnNumber++;
Phase = BattlePhase.PlayerTurn; LastMessage = TurnNumber + "번째 턴 — 아군 차례" + (acted == 0 ? " (적은 교전 범위 밖에서 대기)" : "");
LastMessage = TurnNumber + "번째 턴 — 아군 차례"; SetPhase(BattlePhase.PlayerTurn);
NotifyChanged();
} }
/// <summary>창병 대기: 상대 유닛이 지나간 경로가 대기 중인 창병 반경을 스치면 피해(창병마다 1회).</summary> /// <summary>창병 대기: 상대 유닛이 지나간 경로가 대기 중인 창병 반경을 스치면 피해(창병마다 1회).</summary>
@ -314,16 +335,14 @@ namespace TKS
{ {
if (AliveCount(Faction.Enemy) == 0) if (AliveCount(Faction.Enemy) == 0)
{ {
Phase = BattlePhase.StageClear;
LastMessage = "승리 — 전장의 적을 모두 처치했습니다"; LastMessage = "승리 — 전장의 적을 모두 처치했습니다";
NotifyChanged(); SetPhase(BattlePhase.StageClear);
return true; return true;
} }
if (AliveCount(Faction.Player) == 0) if (AliveCount(Faction.Player) == 0)
{ {
Phase = BattlePhase.Defeat;
LastMessage = "패배 — 아군이 전멸했습니다"; LastMessage = "패배 — 아군이 전멸했습니다";
NotifyChanged(); SetPhase(BattlePhase.Defeat);
return true; return true;
} }
return false; return false;
@ -338,7 +357,7 @@ namespace TKS
public void ForcePhase(BattlePhase p) public void ForcePhase(BattlePhase p)
{ {
Phase = p; SetPhase(p);
NotifyChanged(); NotifyChanged();
} }

View File

@ -7,7 +7,7 @@ namespace TKS
{ {
} }
/// <summary>전장(바닥·테두리·4면 벽 콜라이더).</summary> /// <summary>전장(바닥·격자·테두리·4면 벽 콜라이더). 화면보다 넓어 카메라로 스크롤한다.</summary>
public class Board : MonoBehaviour public class Board : MonoBehaviour
{ {
public Vector2 Size { get; private set; } public Vector2 Size { get; private set; }
@ -31,12 +31,28 @@ namespace TKS
enemyHalf.transform.localScale = new Vector3(w, h * 0.5f, 1f); enemyHalf.transform.localScale = new Vector3(w, h * 0.5f, 1f);
enemyHalf.transform.localPosition = new Vector3(0f, h * 0.25f, 0f); enemyHalf.transform.localPosition = new Vector3(0f, h * 0.25f, 0f);
var mid = ProceduralSprites.NewSprite(transform, "MidLine", ProceduralSprites.Square, new Color(1f, 1f, 1f, 0.12f), 1f, -18); // 격자(4 단위) — 스크롤 체감용
mid.transform.localScale = new Vector3(w, 0.04f, 1f); var gridColor = new Color(1f, 1f, 1f, 0.07f);
const float gridStep = 4f;
for (float x = -w * 0.5f + gridStep; x < w * 0.5f - 0.01f; x += gridStep)
{
var g = ProceduralSprites.NewSprite(transform, "GridV", ProceduralSprites.Square, gridColor, 1f, -18);
g.transform.localPosition = new Vector3(x, 0f, 0f);
g.transform.localScale = new Vector3(0.04f, h, 1f);
}
for (float y = -h * 0.5f + gridStep; y < h * 0.5f - 0.01f; y += gridStep)
{
var g = ProceduralSprites.NewSprite(transform, "GridH", ProceduralSprites.Square, gridColor, 1f, -18);
g.transform.localPosition = new Vector3(0f, y, 0f);
g.transform.localScale = new Vector3(w, 0.04f, 1f);
}
var mid = ProceduralSprites.NewSprite(transform, "MidLine", ProceduralSprites.Square, new Color(1f, 1f, 1f, 0.14f), 1f, -17);
mid.transform.localScale = new Vector3(w, 0.06f, 1f);
// 테두리 // 테두리
var borderColor = new Color(0.85f, 0.75f, 0.45f); var borderColor = new Color(0.85f, 0.75f, 0.45f);
const float t = 0.12f; const float t = 0.16f;
MakeBorder("BorderTop", new Vector2(0f, h * 0.5f + t * 0.5f), new Vector2(w + t * 2f, t), borderColor); MakeBorder("BorderTop", new Vector2(0f, h * 0.5f + t * 0.5f), new Vector2(w + t * 2f, t), borderColor);
MakeBorder("BorderBottom", new Vector2(0f, -h * 0.5f - t * 0.5f), new Vector2(w + t * 2f, t), borderColor); MakeBorder("BorderBottom", new Vector2(0f, -h * 0.5f - t * 0.5f), new Vector2(w + t * 2f, t), borderColor);
MakeBorder("BorderLeft", new Vector2(-w * 0.5f - t * 0.5f, 0f), new Vector2(t, h), borderColor); MakeBorder("BorderLeft", new Vector2(-w * 0.5f - t * 0.5f, 0f), new Vector2(t, h), borderColor);
@ -52,7 +68,7 @@ namespace TKS
void MakeBorder(string name, Vector2 pos, Vector2 size, Color color) void MakeBorder(string name, Vector2 pos, Vector2 size, Color color)
{ {
var sr = ProceduralSprites.NewSprite(transform, name, ProceduralSprites.Square, color, 1f, -17); var sr = ProceduralSprites.NewSprite(transform, name, ProceduralSprites.Square, color, 1f, -16);
sr.transform.localPosition = pos; sr.transform.localPosition = pos;
sr.transform.localScale = new Vector3(size.x, size.y, 1f); sr.transform.localScale = new Vector3(size.x, size.y, 1f);
} }

View File

@ -0,0 +1,137 @@
using UnityEngine;
namespace TKS
{
/// <summary>
/// 넓은 전장용 카메라: 화면보다 큰 전장을 빈 곳 드래그로 스크롤 · 휠로 줌 · 행동 중인 유닛 자동 추적 · 전장 밖으로 못 나가게 고정.
/// </summary>
public class CameraController : MonoBehaviour
{
public Board Board;
public float ViewHeight = 18f;
public float MinViewHeight = 10f;
public float MaxViewHeight = 36f;
public float Margin = 1f;
public float FollowSpeed = 7f;
Camera cam;
Vector2? focusTarget;
Transform followTarget;
public Camera Cam
{
get
{
if (cam == null) cam = GetComponent<Camera>();
return cam;
}
}
public void Init(Board board, float viewHeight, float minViewHeight, float maxViewHeight)
{
Board = board;
MinViewHeight = Mathf.Max(1f, minViewHeight);
MaxViewHeight = Mathf.Max(MinViewHeight, maxViewHeight);
ViewHeight = Mathf.Clamp(viewHeight, MinViewHeight, MaxViewHeight);
Cam.orthographicSize = ViewHeight * 0.5f;
SetPosition(transform.position);
}
/// <summary>화면 1 픽셀이 월드 몇 단위인가(드래그 스크롤 환산).</summary>
public float WorldUnitsPerPixel
{
get { return Cam.orthographicSize * 2f / Mathf.Max(1, Cam.pixelHeight); }
}
public Rect ViewRect
{
get
{
float halfH = Cam.orthographicSize;
float halfW = halfH * Cam.aspect;
Vector2 c = transform.position;
return new Rect(c.x - halfW, c.y - halfH, halfW * 2f, halfH * 2f);
}
}
public void PanBy(Vector2 worldDelta)
{
focusTarget = null;
followTarget = null;
SetPosition((Vector2)transform.position + worldDelta);
}
public void FocusOn(Vector2 worldPos)
{
followTarget = null;
focusTarget = worldPos;
}
public void SnapTo(Vector2 worldPos)
{
followTarget = null;
focusTarget = null;
SetPosition(worldPos);
}
public void Follow(Transform t)
{
focusTarget = null;
followTarget = t;
}
public void StopFollow()
{
followTarget = null;
focusTarget = null;
}
public void Zoom(float factor)
{
SetViewHeight(ViewHeight * factor);
}
public void SetViewHeight(float height)
{
ViewHeight = Mathf.Clamp(height, MinViewHeight, MaxViewHeight);
Cam.orthographicSize = ViewHeight * 0.5f;
SetPosition(transform.position);
}
void LateUpdate()
{
Vector2? goal = null;
if (followTarget != null && followTarget.gameObject.activeInHierarchy) goal = (Vector2)followTarget.position;
else if (focusTarget.HasValue) goal = focusTarget;
if (!goal.HasValue) return;
Vector2 target = Clamp(goal.Value);
Vector2 cur = transform.position;
float k = 1f - Mathf.Exp(-FollowSpeed * Time.deltaTime);
Vector2 next = Vector2.Lerp(cur, target, k);
SetPosition(next);
if (focusTarget.HasValue && Vector2.Distance(next, target) < 0.02f) focusTarget = null;
}
void SetPosition(Vector2 p)
{
p = Clamp(p);
transform.position = new Vector3(p.x, p.y, -10f);
}
public Vector2 Clamp(Vector2 p)
{
if (Board == null) return p;
float halfH = Cam.orthographicSize;
float halfW = halfH * Cam.aspect;
var r = Board.Rect;
float minX = r.xMin - Margin + halfW;
float maxX = r.xMax + Margin - halfW;
float minY = r.yMin - Margin + halfH;
float maxY = r.yMax + Margin - halfH;
p.x = minX > maxX ? 0f : Mathf.Clamp(p.x, minX, maxX);
p.y = minY > maxY ? 0f : Mathf.Clamp(p.y, minY, maxY);
return p;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e83ae9280a929b244832db516173bdf0

View File

@ -3,10 +3,10 @@ using UnityEngine;
namespace TKS namespace TKS
{ {
/// <summary>적군 슬링샷 결정: 가장 가까운 아군을 향해 당기고, 정확도에 따라 흔들린다.</summary> /// <summary>적군 슬링샷 결정: 교전 범위 안의 가장 가까운 아군을 향해 당기고, 정확도에 따라 흔들린다. 범위 밖이면 대기(null).</summary>
public static class EnemyAI public static class EnemyAI
{ {
public static Vector2 Decide(Unit e, IReadOnlyList<Unit> targets, float accuracy, System.Random rng, Board board) public static Vector2? Decide(Unit e, IReadOnlyList<Unit> targets, float accuracy, float aggroRange, System.Random rng, Board board)
{ {
Unit nearest = null; Unit nearest = null;
float best = float.MaxValue; float best = float.MaxValue;
@ -21,7 +21,8 @@ namespace TKS
nearest = t; nearest = t;
} }
} }
if (nearest == null) return e.Position; if (nearest == null) return null;
if (best > aggroRange) return null; // 넓은 전장: 교전 범위 밖이면 자리 지킴
Vector2 toT = nearest.Position - e.Position; Vector2 toT = nearest.Position - e.Position;
float dist = toT.magnitude; float dist = toT.magnitude;

View File

@ -10,6 +10,7 @@ namespace TKS
public int StartStageIndex = 0; public int StartStageIndex = 0;
public BattleManager Battle { get; private set; } public BattleManager Battle { get; private set; }
public CameraController CameraCtrl { get; private set; }
void Awake() void Awake()
{ {
@ -33,54 +34,47 @@ namespace TKS
cam.transform.position = new Vector3(0f, 0f, -10f); cam.transform.position = new Vector3(0f, 0f, -10f);
cam.nearClipPlane = 0.1f; cam.nearClipPlane = 0.1f;
cam.farClipPlane = 100f; cam.farClipPlane = 100f;
var fit = cam.GetComponent<CameraFitter>();
if (fit == null) fit = cam.gameObject.AddComponent<CameraFitter>();
var board = new GameObject("Board").AddComponent<Board>(); var board = new GameObject("Board").AddComponent<Board>();
board.Build(Config.boardSize); board.Build(Config.boardSize);
fit.Board = board;
fit.Fit(); CameraCtrl = cam.GetComponent<CameraController>();
if (CameraCtrl == null) CameraCtrl = cam.gameObject.AddComponent<CameraController>();
CameraCtrl.Init(board, Config.cameraViewHeight, Config.cameraMinViewHeight, Config.cameraMaxViewHeight);
Battle = new GameObject("BattleManager").AddComponent<BattleManager>(); Battle = new GameObject("BattleManager").AddComponent<BattleManager>();
Battle.Configure(Config, board); Battle.Configure(Config, board);
var hud = new GameObject("HUD").AddComponent<BattleHUD>(); var hud = new GameObject("HUD").AddComponent<BattleHUD>();
hud.Build(Battle, cam); hud.Build(Battle, cam, CameraCtrl);
Battle.Hud = hud; Battle.Hud = hud;
var input = new GameObject("SlingshotInput").AddComponent<SlingshotInput>(); var input = new GameObject("SlingshotInput").AddComponent<SlingshotInput>();
input.Init(Battle, cam, hud); input.Init(Battle, cam, hud, CameraCtrl);
Battle.Sling = input; Battle.Sling = input;
// 카메라 연동: 행동 중인 유닛 추적 · 아군 턴 시작 시 아군 쪽으로
var camCtrl = CameraCtrl;
var battle = Battle;
battle.UnitActing += u => camCtrl.Follow(u.transform);
battle.PhaseChanged += phase =>
{
if (phase == BattlePhase.PlayerTurn) camCtrl.FocusOn(PlayerFocusPoint(battle));
else if (phase == BattlePhase.StageClear || phase == BattlePhase.Defeat) camCtrl.StopFollow();
};
battle.StageStarted += () => camCtrl.SnapTo(PlayerFocusPoint(battle));
if (AutoStart) Battle.StartStage(StartStageIndex); if (AutoStart) Battle.StartStage(StartStageIndex);
} }
}
/// <summary>화면 비율이 달라도 전장 전체가 보이도록 직교 카메라 크기를 맞춘다.</summary> /// <summary>아군 무게중심에서 적 쪽으로 조금 올린 지점.</summary>
public class CameraFitter : MonoBehaviour static Vector2 PlayerFocusPoint(BattleManager battle)
{
public Board Board;
public float Margin = 0.5f;
Camera cam;
void Awake()
{ {
cam = GetComponent<Camera>(); var players = battle.AliveUnits(Faction.Player);
} if (players.Count == 0) return Vector2.zero;
Vector2 sum = Vector2.zero;
void LateUpdate() foreach (var p in players) sum += p.Position;
{ return sum / players.Count + new Vector2(0f, battle.Config.cameraViewHeight * 0.2f);
Fit();
}
public void Fit()
{
if (cam == null) cam = GetComponent<Camera>();
if (Board == null || cam == null) return;
float halfW = Board.Size.x * 0.5f + Margin;
float halfH = Board.Size.y * 0.5f + Margin + 1.2f; // 위아래 HUD 여유
float aspect = Mathf.Max(0.1f, cam.aspect);
cam.orthographicSize = Mathf.Max(halfH, halfW / aspect);
} }
} }
} }

View File

@ -5,6 +5,7 @@ namespace TKS
{ {
/// <summary> /// <summary>
/// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격. /// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격.
/// 빈 곳 드래그 = 전장 스크롤 · 휠 = 줌 · 미니맵 탭 = 그 지점으로 이동.
/// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기. /// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기.
/// </summary> /// </summary>
public class SlingshotInput : MonoBehaviour public class SlingshotInput : MonoBehaviour
@ -12,12 +13,15 @@ namespace TKS
BattleManager battle; BattleManager battle;
Camera cam; Camera cam;
BattleHUD hud; BattleHUD hud;
CameraController camCtrl;
Unit selected; Unit selected;
bool dragging; bool dragging;
bool panning;
bool emptyPress; bool emptyPress;
bool previewValid; bool previewValid;
Vector2 pressWorld; Vector2 lastScreen;
float panPixels;
Vector2 currentTarget; Vector2 currentTarget;
LineRenderer aimLine; LineRenderer aimLine;
@ -30,11 +34,17 @@ namespace TKS
get { return selected; } get { return selected; }
} }
public void Init(BattleManager b, Camera c, BattleHUD h) float TapPixels
{
get { return Mathf.Max(8f, Screen.height * 0.012f); }
}
public void Init(BattleManager b, Camera c, BattleHUD h, CameraController cc)
{ {
battle = b; battle = b;
cam = c; cam = c;
hud = h; hud = h;
camCtrl = cc;
aimLine = MakeLine("AimLine", 0.08f, new Color(1f, 1f, 1f, 0.9f), 10); aimLine = MakeLine("AimLine", 0.08f, new Color(1f, 1f, 1f, 0.9f), 10);
pullLine = MakeLine("PullLine", 0.035f, new Color(1f, 1f, 1f, 0.35f), 9); pullLine = MakeLine("PullLine", 0.035f, new Color(1f, 1f, 1f, 0.35f), 9);
targetRing = ProceduralSprites.NewSprite(transform, "TargetRing", ProceduralSprites.Ring, Color.white, 0.7f, 11); targetRing = ProceduralSprites.NewSprite(transform, "TargetRing", ProceduralSprites.Ring, Color.white, 0.7f, 11);
@ -59,9 +69,9 @@ namespace TKS
return lr; return lr;
} }
Vector2 PointerWorld() Vector2 ScreenToWorld(Vector2 screen)
{ {
Vector3 p = Input.mousePosition; Vector3 p = screen;
p.z = -cam.transform.position.z; p.z = -cam.transform.position.z;
return cam.ScreenToWorldPoint(p); return cam.ScreenToWorldPoint(p);
} }
@ -76,16 +86,36 @@ namespace TKS
return; return;
} }
if (Input.GetMouseButtonDown(0)) OnPress(PointerWorld()); Vector2 screen = Input.mousePosition;
else if (dragging && Input.GetMouseButton(0)) OnDrag(PointerWorld()); if (Input.GetMouseButtonDown(0)) OnPress(screen);
if (Input.GetMouseButtonUp(0)) OnRelease(PointerWorld()); else if (Input.GetMouseButton(0))
{
if (dragging) OnDrag(ScreenToWorld(screen));
else if (panning) OnPan(screen);
}
if (Input.GetMouseButtonUp(0)) OnRelease(screen);
float wheel = Input.mouseScrollDelta.y;
if (Mathf.Abs(wheel) > 0.01f && camCtrl != null) camCtrl.Zoom(1f - wheel * 0.1f);
} }
void OnPress(Vector2 w) void OnPress(Vector2 screen)
{ {
pressWorld = w; lastScreen = screen;
panPixels = 0f;
dragging = false;
panning = false;
emptyPress = false; emptyPress = false;
previewValid = false; previewValid = false;
Vector2 mapWorld;
if (hud != null && camCtrl != null && hud.TryMinimapTap(screen, out mapWorld))
{
camCtrl.FocusOn(mapWorld);
return;
}
Vector2 w = ScreenToWorld(screen);
var u = battle.UnitNear(w, Faction.Player); var u = battle.UnitNear(w, Faction.Player);
if (u == null) u = battle.UnitNear(w, null); if (u == null) u = battle.UnitNear(w, null);
Select(u); Select(u);
@ -93,10 +123,21 @@ namespace TKS
{ {
dragging = true; dragging = true;
} }
else else if (u == null)
{ {
dragging = false; panning = true;
emptyPress = (u == null); emptyPress = true;
}
}
void OnPan(Vector2 screen)
{
Vector2 d = screen - lastScreen;
lastScreen = screen;
panPixels += d.magnitude;
if (panPixels > TapPixels && camCtrl != null)
{
camCtrl.PanBy(-d * camCtrl.WorldUnitsPerPixel);
} }
} }
@ -118,7 +159,7 @@ namespace TKS
ShowPreview(selected, currentTarget, w); ShowPreview(selected, currentTarget, w);
} }
void OnRelease(Vector2 w) void OnRelease(Vector2 screen)
{ {
if (dragging && selected != null && previewValid && battle.CanAct(selected)) if (dragging && selected != null && previewValid && battle.CanAct(selected))
{ {
@ -128,7 +169,7 @@ namespace TKS
Select(null); Select(null);
battle.RequestPlayerAction(u, t); battle.RequestPlayerAction(u, t);
} }
else if (emptyPress && Vector2.Distance(w, pressWorld) < 0.3f) else if (emptyPress && panPixels <= TapPixels)
{ {
battle.OnEmptyTap(); battle.OnEmptyTap();
} }
@ -137,6 +178,7 @@ namespace TKS
HidePreview(); HidePreview();
} }
dragging = false; dragging = false;
panning = false;
emptyPress = false; emptyPress = false;
previewValid = false; previewValid = false;
} }

View File

@ -31,10 +31,11 @@ namespace TKS
baseColor = u.Faction == Faction.Player ? PlayerColor : EnemyColor; baseColor = u.Faction == Faction.Player ? PlayerColor : EnemyColor;
float d = u.Radius * 2f; float d = u.Radius * 2f;
overwatchArea = ProceduralSprites.NewSprite(transform, "OverwatchArea", ProceduralSprites.Circle, new Color(0.3f, 0.65f, 1f, 0.16f), u.AttackRange * 2f, -6); // 창병 대기 반경 표시(창병만 · 발동 중에만 오브젝트 통째로 켬)
overwatchArea.enabled = false; overwatchArea = ProceduralSprites.NewSprite(transform, "OverwatchArea", ProceduralSprites.Circle, new Color(0.3f, 0.65f, 1f, 0.16f), u.Type == UnitType.Spear ? u.AttackRange * 2f : 0f, -6);
var owRing = ProceduralSprites.NewSprite(overwatchArea.transform, "Ring", ProceduralSprites.Ring, new Color(0.5f, 0.8f, 1f, 0.6f), 1f, -5); var owRing = ProceduralSprites.NewSprite(overwatchArea.transform, "Ring", ProceduralSprites.Ring, new Color(0.5f, 0.8f, 1f, 0.6f), 1f, -5);
owRing.transform.localScale = Vector3.one; owRing.transform.localScale = Vector3.one;
overwatchArea.gameObject.SetActive(false);
if (u.IsBoss) if (u.IsBoss)
{ {
@ -79,7 +80,8 @@ namespace TKS
if (flash == null) body.color = c; if (flash == null) body.color = c;
mark.color = dim ? new Color(1f, 1f, 1f, 0.6f) : Color.white; mark.color = dim ? new Color(1f, 1f, 1f, 0.6f) : Color.white;
overwatchArea.enabled = unit.Overwatch; bool ow = unit.Overwatch && unit.Type == UnitType.Spear && unit.IsAlive;
if (overwatchArea.gameObject.activeSelf != ow) overwatchArea.gameObject.SetActive(ow);
badge.gameObject.SetActive(unit.BonusMoveAvailable && unit.IsAlive); badge.gameObject.SetActive(unit.BonusMoveAvailable && unit.IsAlive);
} }
@ -123,7 +125,7 @@ namespace TKS
{ {
selectRing.enabled = false; selectRing.enabled = false;
highlightRing.enabled = false; highlightRing.enabled = false;
overwatchArea.enabled = false; overwatchArea.gameObject.SetActive(false);
float t = 0f; float t = 0f;
const float dur = 0.45f; const float dur = 0.45f;
Vector3 s0 = transform.localScale; Vector3 s0 = transform.localScale;

View File

@ -220,5 +220,35 @@ namespace TKS.Tests
Assert.AreEqual(BattlePhase.StageClear, bm.Phase); Assert.AreEqual(BattlePhase.StageClear, bm.Phase);
Assert.IsFalse(e.IsAlive); Assert.IsFalse(e.IsAlive);
} }
[UnityTest]
public IEnumerator EnemyAI_HoldsOutsideAggroRange_ActsInside()
{
yield return Fresh();
var p = bm.SpawnUnit(UnitType.Infantry, Faction.Player, new Vector2(0f, -13f), "아군");
var far = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, 10f), "먼적");
var near = bm.SpawnUnit(UnitType.Infantry, Faction.Enemy, new Vector2(0f, -5f), "가까운적");
var players = bm.AliveUnits(Faction.Player);
Assert.IsFalse(EnemyAI.Decide(far, players, 1f, 11f, bm.Rng, bm.Board).HasValue, "교전 범위 밖 → 대기");
Assert.IsTrue(EnemyAI.Decide(near, players, 1f, 11f, bm.Rng, bm.Board).HasValue, "교전 범위 안 → 행동");
Assert.IsTrue(p.IsAlive);
}
[UnityTest]
public IEnumerator Camera_BoardWiderThanView_ClampsAndPans()
{
var cc = Camera.main.GetComponent<CameraController>();
Assert.IsNotNull(cc, "넓은 전장 카메라 컨트롤러");
Assert.Greater(bm.Config.boardSize.y, bm.Config.cameraViewHeight, "전장이 화면보다 넓어야 스크롤이 의미 있다");
var r = bm.Board.Rect;
cc.SnapTo(new Vector2(1000f, 1000f));
var v = cc.ViewRect;
Assert.LessOrEqual(v.yMax, r.yMax + cc.Margin + 0.01f, "전장 위쪽 밖으로 못 나감");
Assert.LessOrEqual(v.xMax, r.xMax + cc.Margin + 0.01f + Mathf.Max(0f, v.width - r.width), "전장 오른쪽 밖으로 못 나감");
cc.SnapTo(Vector2.zero);
cc.PanBy(new Vector2(0f, 2f));
Assert.AreEqual(2f, cc.transform.position.y, 0.01f, "빈 곳 드래그 스크롤");
yield return null;
}
} }
} }

View File

@ -86,7 +86,7 @@ PlayerSettings:
androidApplicationEntry: 2 androidApplicationEntry: 2
defaultIsNativeResolution: 1 defaultIsNativeResolution: 1
macRetinaSupport: 1 macRetinaSupport: 1
runInBackground: 0 runInBackground: 1
muteOtherAudioSources: 0 muteOtherAudioSources: 0
Prepare IOS For Recording: 0 Prepare IOS For Recording: 0
Force IOS Speakers When Recording: 0 Force IOS Speakers When Recording: 0

View File

@ -12,6 +12,8 @@ Unity 6000.3.23f1 · 2D · 도형 플레이스홀더(아트 리소스는 별도
- 아군 유닛을 누른 채 **뒤로 당겼다 놓으면** 반대 방향으로 날아간다(슬링샷). 하얀 선 = 날아갈 경로, 끝의 링 = 최종 목표점. 링이 주황색이면 목표점이 적 위에 있다(명중). - 아군 유닛을 누른 채 **뒤로 당겼다 놓으면** 반대 방향으로 날아간다(슬링샷). 하얀 선 = 날아갈 경로, 끝의 링 = 최종 목표점. 링이 주황색이면 목표점이 적 위에 있다(명중).
- 이동과 공격은 하나의 조작이다. 경로 위에 적이 있으면 병종 규칙대로 공격한다. 주황 링이 켜진 적 = 이번 조작으로 공격하게 될 적. - 이동과 공격은 하나의 조작이다. 경로 위에 적이 있으면 병종 규칙대로 공격한다. 주황 링이 켜진 적 = 이번 조작으로 공격하게 될 적.
- 전장(20×32)은 화면보다 넓다. **빈 곳을 드래그하면 스크롤**, 마우스 휠 = 줌, 오른쪽 위 **미니맵을 탭하면 그 지점으로 이동**. 행동 중인 유닛은 카메라가 따라간다.
- 적은 교전 범위(스테이지별 11~12) 안에 아군이 들어와야 움직인다. 멀리 있는 부대는 자리를 지키므로 부대 단위로 각개격파할 수 있다.
- 아군 전원이 행동하면 자동으로 적군 턴이 진행된다. 보병 추가 이동만 남았을 때 빈 곳을 탭하면 건너뛴다. - 아군 전원이 행동하면 자동으로 적군 턴이 진행된다. 보병 추가 이동만 남았을 때 빈 곳을 탭하면 건너뛴다.
- 승리/패배 화면은 탭으로 다음 전투·재도전. - 승리/패배 화면은 탭으로 다음 전투·재도전.
- 스킬 아이콘은 자리만(초기 단계 미구현). - 스킬 아이콘은 자리만(초기 단계 미구현).
@ -35,7 +37,7 @@ Unity 6000.3.23f1 · 2D · 도형 플레이스홀더(아트 리소스는 별도
## CLI ## CLI
``` ```
# 컴파일 + 설정 자산·씬 생성 # 컴파일 + 설정 자산·씬 생성 (BuildFresh = 기본값 재시드까지)
Unity.exe -batchmode -nographics -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.Build -quit -logFile <log> Unity.exe -batchmode -nographics -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.Build -quit -logFile <log>
# 플레이모드 테스트 # 플레이모드 테스트
@ -47,6 +49,6 @@ Unity.exe -projectPath <proj> -executeMethod TKS.EditorTools.ProjectSetup.AutoPl
## 구조 ## 구조
- `Assets/Scripts/Runtime/` — `GameBootstrap`(씬 구성) · `BattleManager`(턴·승패) · `ActionResolver`(병종 규칙) · `Combat`(피해 공식·기하 질의) · `SlingshotInput`(조작) · `EnemyAI` · `BattleHUD` · `Unit`/`UnitView` · `Board` - `Assets/Scripts/Runtime/` — `GameBootstrap`(씬 구성) · `BattleManager`(턴·승패) · `ActionResolver`(병종 규칙) · `Combat`(피해 공식·기하 질의) · `SlingshotInput`(조작·스크롤·줌) · `CameraController`(넓은 전장 카메라) · `EnemyAI`(교전 범위) · `BattleHUD`(HUD·미니맵) · `Unit`/`UnitView` · `Board`
- `Assets/Scripts/Editor/` — `ProjectSetup`(자동 구성) · `BalanceDefaults`(최초 시드) - `Assets/Scripts/Editor/` — `ProjectSetup`(자동 구성) · `BalanceDefaults`(최초 시드)
- `Assets/Tests/PlayMode/` — 병종 규칙·턴 흐름·승리 판정 테스트 - `Assets/Tests/PlayMode/` — 병종 규칙·턴 흐름·승리 판정 테스트