넓은 전장(20×32)·스크롤 카메라·미니맵·적 교전 범위 — PD 추가 지시 「스크롤 가능한 비교적 넓은 전장」 · 빈 곳 드래그 스크롤·휠 줌·행동 유닛 추적·미니맵 탭 이동 · 적은 교전 범위 안에서만 행동 · 스테이지 4종 전위/중군/후군 재배치 · 창병 대기 링 상시 표시 결함 수정 · 자동 플레이 timeScale 가속 · 테스트 14종
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
5135d828e2
commit
ceaec991dd
|
|
@ -69,13 +69,16 @@ MonoBehaviour:
|
|||
minDragDistance: 0.35
|
||||
finalHitTolerance: 0.15
|
||||
moveSpeed: 9
|
||||
boardSize: {x: 12, y: 16}
|
||||
boardSize: {x: 20, y: 32}
|
||||
cameraViewHeight: 18
|
||||
cameraMinViewHeight: 10
|
||||
cameraMaxViewHeight: 36
|
||||
playerSpawnPoints:
|
||||
- {x: -4, y: -5.5}
|
||||
- {x: -2, y: -5.5}
|
||||
- {x: 0, y: -5.5}
|
||||
- {x: 2, y: -5.5}
|
||||
- {x: 4, y: -5.5}
|
||||
- {x: -4, y: -13}
|
||||
- {x: -2, y: -13}
|
||||
- {x: 0, y: -13}
|
||||
- {x: 2, y: -13}
|
||||
- {x: 4, y: -13}
|
||||
heroes:
|
||||
- name: "\uC720\uBE44"
|
||||
type: 0
|
||||
|
|
@ -101,184 +104,236 @@ MonoBehaviour:
|
|||
- title: "\uD669\uAC74\uC801\uC758 \uB09C"
|
||||
subtitle: "184\uB144"
|
||||
aiAccuracy: 0.5
|
||||
aiAggroRange: 11
|
||||
enemies:
|
||||
- name: "\uD669\uAC74\uC801 \uBCF4\uBCD1"
|
||||
type: 0
|
||||
position: {x: -3.5, y: 2.5}
|
||||
position: {x: -3, y: -6}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uD669\uAC74\uC801 \uBCF4\uBCD1"
|
||||
type: 0
|
||||
position: {x: 3.5, y: 2.5}
|
||||
position: {x: 3, y: -6}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uD669\uAC74\uC801 \uCC3D\uBCD1"
|
||||
type: 3
|
||||
position: {x: 0, y: 2}
|
||||
position: {x: 0, y: -4.5}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uD669\uAC74\uC801 \uAD81\uBCD1"
|
||||
type: 2
|
||||
position: {x: -2, y: 5}
|
||||
position: {x: -4, y: 1}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uD669\uAC74\uC801 \uAD81\uBCD1"
|
||||
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
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC7A5\uAC01"
|
||||
type: 0
|
||||
position: {x: 0, y: 6.5}
|
||||
position: {x: 0, y: 10}
|
||||
hpMult: 1.3
|
||||
atkMult: 1.2
|
||||
isBoss: 1
|
||||
- title: "\uD638\uB85C\uAD00 \uC804\uD22C"
|
||||
subtitle: "190\uB144"
|
||||
aiAccuracy: 0.6
|
||||
aiAggroRange: 11
|
||||
enemies:
|
||||
- name: "\uB3D9\uD0C1\uAD70 \uCC3D\uBCD1"
|
||||
type: 3
|
||||
position: {x: -2, y: 2.2}
|
||||
position: {x: -2.5, y: -5}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uB3D9\uD0C1\uAD70 \uCC3D\uBCD1"
|
||||
type: 3
|
||||
position: {x: 2, y: 2.2}
|
||||
position: {x: 2.5, y: -5}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uB3D9\uD0C1\uAD70 \uAE30\uBCD1"
|
||||
type: 1
|
||||
position: {x: -4, y: 3.5}
|
||||
position: {x: -6, y: -1}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uB3D9\uD0C1\uAD70 \uAE30\uBCD1"
|
||||
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
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uB3D9\uD0C1\uAD70 \uAD81\uBCD1"
|
||||
type: 2
|
||||
position: {x: -2.5, y: 5.5}
|
||||
position: {x: -3, y: 3}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uB3D9\uD0C1\uAD70 \uAD81\uBCD1"
|
||||
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
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC5EC\uD3EC"
|
||||
type: 1
|
||||
position: {x: 0, y: 4.5}
|
||||
position: {x: 0, y: 11}
|
||||
hpMult: 1.6
|
||||
atkMult: 1.5
|
||||
isBoss: 1
|
||||
- title: "\uC801\uBCBD\uB300\uC804"
|
||||
subtitle: "208\uB144"
|
||||
aiAccuracy: 0.65
|
||||
aiAggroRange: 12
|
||||
enemies:
|
||||
- name: "\uC870\uC870\uAD70 \uBCF4\uBCD1"
|
||||
type: 0
|
||||
position: {x: -4, y: 2.5}
|
||||
position: {x: -5, y: -6}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC870\uC870\uAD70 \uBCF4\uBCD1"
|
||||
type: 0
|
||||
position: {x: 0, y: 2}
|
||||
position: {x: 0, y: -5}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC870\uC870\uAD70 \uBCF4\uBCD1"
|
||||
type: 0
|
||||
position: {x: 4, y: 2.5}
|
||||
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}
|
||||
position: {x: 5, y: -6}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC870\uC870\uAD70 \uAE30\uBCD1"
|
||||
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
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC870\uC870"
|
||||
type: 0
|
||||
position: {x: 0, y: 6.8}
|
||||
position: {x: 0, y: 12}
|
||||
hpMult: 1.5
|
||||
atkMult: 1.3
|
||||
isBoss: 1
|
||||
- title: "\uC624\uC7A5\uC6D0 \uC804\uD22C"
|
||||
subtitle: "234\uB144"
|
||||
aiAccuracy: 0.7
|
||||
aiAggroRange: 12
|
||||
enemies:
|
||||
- name: "\uC704\uAD70 \uCC3D\uBCD1"
|
||||
type: 3
|
||||
position: {x: -2.5, y: 2.2}
|
||||
position: {x: -3, y: -6}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC704\uAD70 \uCC3D\uBCD1"
|
||||
type: 3
|
||||
position: {x: 2.5, y: 2.2}
|
||||
position: {x: 3, y: -6}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC704\uAD70 \uBCF4\uBCD1"
|
||||
type: 0
|
||||
position: {x: -4.5, y: 3.5}
|
||||
position: {x: -7, y: -2}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC704\uAD70 \uBCF4\uBCD1"
|
||||
type: 0
|
||||
position: {x: 4.5, y: 3.5}
|
||||
position: {x: 7, y: -2}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC704\uAD70 \uAE30\uBCD1"
|
||||
type: 1
|
||||
position: {x: -1.5, y: 4.5}
|
||||
position: {x: -2, y: 2}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC704\uAD70 \uAE30\uBCD1"
|
||||
type: 1
|
||||
position: {x: 1.5, y: 4.5}
|
||||
position: {x: 2, y: 2}
|
||||
hpMult: 1
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC704\uAD70 \uAD81\uBCD1"
|
||||
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
|
||||
atkMult: 1
|
||||
isBoss: 0
|
||||
- name: "\uC0AC\uB9C8\uC758"
|
||||
type: 0
|
||||
position: {x: 0, y: 7.2}
|
||||
position: {x: 0, y: 13}
|
||||
hpMult: 1.6
|
||||
atkMult: 1.3
|
||||
isBoss: 1
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ RenderSettings:
|
|||
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_AmbientIntensity: 1
|
||||
m_AmbientMode: 0
|
||||
m_AmbientMode: 3
|
||||
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_FlareStrength: 1
|
||||
m_FlareFadeSpeed: 3
|
||||
|
|
@ -50,7 +50,7 @@ LightmapSettings:
|
|||
m_IndirectOutputScale: 1
|
||||
m_AlbedoBoost: 1
|
||||
m_EnvironmentLightingMode: 0
|
||||
m_EnableBakedLightmaps: 1
|
||||
m_EnableBakedLightmaps: 0
|
||||
m_EnableRealtimeLightmaps: 0
|
||||
m_LightmapEditorSettings:
|
||||
serializedVersion: 12
|
||||
|
|
@ -119,7 +119,7 @@ NavMeshSettings:
|
|||
debug:
|
||||
m_Flags: 0
|
||||
m_NavMeshData: {fileID: 0}
|
||||
--- !u!1 &930732900
|
||||
--- !u!1 &1288297926
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
|
|
@ -127,9 +127,56 @@ GameObject:
|
|||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 930732903}
|
||||
- component: {fileID: 930732902}
|
||||
- component: {fileID: 930732901}
|
||||
- component: {fileID: 1288297928}
|
||||
- component: {fileID: 1288297927}
|
||||
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_Name: Main Camera
|
||||
m_TagString: MainCamera
|
||||
|
|
@ -137,21 +184,21 @@ GameObject:
|
|||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!81 &930732901
|
||||
--- !u!81 &1938987433
|
||||
AudioListener:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 930732900}
|
||||
m_GameObject: {fileID: 1938987432}
|
||||
m_Enabled: 1
|
||||
--- !u!20 &930732902
|
||||
--- !u!20 &1938987434
|
||||
Camera:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 930732900}
|
||||
m_GameObject: {fileID: 1938987432}
|
||||
m_Enabled: 1
|
||||
serializedVersion: 2
|
||||
m_ClearFlags: 2
|
||||
|
|
@ -196,13 +243,13 @@ Camera:
|
|||
m_OcclusionCulling: 1
|
||||
m_StereoConvergence: 10
|
||||
m_StereoSeparation: 0.022
|
||||
--- !u!4 &930732903
|
||||
--- !u!4 &1938987435
|
||||
Transform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 930732900}
|
||||
m_GameObject: {fileID: 1938987432}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -10}
|
||||
|
|
@ -211,56 +258,9 @@ Transform:
|
|||
m_Children: []
|
||||
m_Father: {fileID: 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
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 930732903}
|
||||
- {fileID: 1640921814}
|
||||
- {fileID: 1938987435}
|
||||
- {fileID: 1288297928}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ namespace TKS.EditorTools
|
|||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public static class BalanceDefaults
|
||||
{
|
||||
|
|
@ -44,10 +44,13 @@ namespace TKS.EditorTools
|
|||
c.finalHitTolerance = 0.15f;
|
||||
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[]
|
||||
{
|
||||
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[]
|
||||
|
|
@ -61,38 +64,49 @@ namespace TKS.EditorTools
|
|||
|
||||
c.stages = new[]
|
||||
{
|
||||
Stage("황건적의 난", "184년", 0.5f,
|
||||
E("황건적 보병", UnitType.Infantry, -3.5f, 2.5f),
|
||||
E("황건적 보병", UnitType.Infantry, 3.5f, 2.5f),
|
||||
E("황건적 창병", UnitType.Spear, 0f, 2.0f),
|
||||
E("황건적 궁병", UnitType.Archer, -2f, 5f),
|
||||
E("황건적 궁병", UnitType.Archer, 2f, 5f),
|
||||
Boss("장각", UnitType.Infantry, 0f, 6.5f, 1.3f, 1.2f)),
|
||||
Stage("호로관 전투", "190년", 0.6f,
|
||||
E("동탁군 창병", UnitType.Spear, -2f, 2.2f),
|
||||
E("동탁군 창병", UnitType.Spear, 2f, 2.2f),
|
||||
E("동탁군 기병", UnitType.Cavalry, -4f, 3.5f),
|
||||
E("동탁군 기병", UnitType.Cavalry, 4f, 3.5f),
|
||||
E("동탁군 궁병", UnitType.Archer, -2.5f, 5.5f),
|
||||
E("동탁군 궁병", UnitType.Archer, 2.5f, 5.5f),
|
||||
Boss("여포", UnitType.Cavalry, 0f, 4.5f, 1.6f, 1.5f)),
|
||||
Stage("적벽대전", "208년", 0.65f,
|
||||
E("조조군 보병", UnitType.Infantry, -4f, 2.5f),
|
||||
E("조조군 보병", UnitType.Infantry, 0f, 2.0f),
|
||||
E("조조군 보병", UnitType.Infantry, 4f, 2.5f),
|
||||
E("조조군 궁병", UnitType.Archer, -2.5f, 5f),
|
||||
E("조조군 궁병", UnitType.Archer, 2.5f, 5f),
|
||||
E("조조군 기병", UnitType.Cavalry, 0f, 4.2f),
|
||||
Boss("조조", UnitType.Infantry, 0f, 6.8f, 1.5f, 1.3f)),
|
||||
Stage("오장원 전투", "234년", 0.7f,
|
||||
E("위군 창병", UnitType.Spear, -2.5f, 2.2f),
|
||||
E("위군 창병", UnitType.Spear, 2.5f, 2.2f),
|
||||
E("위군 보병", UnitType.Infantry, -4.5f, 3.5f),
|
||||
E("위군 보병", UnitType.Infantry, 4.5f, 3.5f),
|
||||
E("위군 기병", UnitType.Cavalry, -1.5f, 4.5f),
|
||||
E("위군 기병", UnitType.Cavalry, 1.5f, 4.5f),
|
||||
E("위군 궁병", UnitType.Archer, 0f, 6f),
|
||||
Boss("사마의", UnitType.Infantry, 0f, 7.2f, 1.6f, 1.3f)),
|
||||
Stage("황건적의 난", "184년", 0.5f, 11f,
|
||||
// 전위
|
||||
E("황건적 보병", UnitType.Infantry, -3f, -6f),
|
||||
E("황건적 보병", UnitType.Infantry, 3f, -6f),
|
||||
E("황건적 창병", UnitType.Spear, 0f, -4.5f),
|
||||
// 중군
|
||||
E("황건적 궁병", UnitType.Archer, -4f, 1f),
|
||||
E("황건적 궁병", UnitType.Archer, 4f, 1f),
|
||||
E("황건적 보병", UnitType.Infantry, 0f, 2.5f),
|
||||
// 후군
|
||||
E("황건적 창병", UnitType.Spear, -2.5f, 8f),
|
||||
Boss("장각", UnitType.Infantry, 0f, 10f, 1.3f, 1.2f)),
|
||||
Stage("호로관 전투", "190년", 0.6f, 11f,
|
||||
E("동탁군 창병", UnitType.Spear, -2.5f, -5f),
|
||||
E("동탁군 창병", UnitType.Spear, 2.5f, -5f),
|
||||
E("동탁군 기병", UnitType.Cavalry, -6f, -1f),
|
||||
E("동탁군 기병", UnitType.Cavalry, 6f, -1f),
|
||||
E("동탁군 보병", UnitType.Infantry, 0f, 1f),
|
||||
E("동탁군 궁병", UnitType.Archer, -3f, 3f),
|
||||
E("동탁군 궁병", UnitType.Archer, 3f, 3f),
|
||||
E("동탁군 기병", UnitType.Cavalry, 0f, 7f),
|
||||
Boss("여포", UnitType.Cavalry, 0f, 11f, 1.6f, 1.5f)),
|
||||
Stage("적벽대전", "208년", 0.65f, 12f,
|
||||
E("조조군 보병", UnitType.Infantry, -5f, -6f),
|
||||
E("조조군 보병", UnitType.Infantry, 0f, -5f),
|
||||
E("조조군 보병", UnitType.Infantry, 5f, -6f),
|
||||
E("조조군 기병", UnitType.Cavalry, -3f, 0f),
|
||||
E("조조군 기병", UnitType.Cavalry, 3f, 0f),
|
||||
E("조조군 궁병", UnitType.Archer, -4f, 5f),
|
||||
E("조조군 궁병", UnitType.Archer, 4f, 5f),
|
||||
E("조조군 창병", UnitType.Spear, 0f, 7f),
|
||||
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 };
|
||||
}
|
||||
|
||||
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) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,17 @@ namespace TKS.EditorTools
|
|||
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)
|
||||
{
|
||||
if (!AssetDatabase.IsValidFolder("Assets/Resources")) AssetDatabase.CreateFolder("Assets", "Resources");
|
||||
|
|
@ -81,6 +92,7 @@ namespace TKS.EditorTools
|
|||
PlayerSettings.companyName = "NerdNavis";
|
||||
PlayerSettings.productName = "ThreeKingdomsSlingshot";
|
||||
PlayerSettings.defaultInterfaceOrientation = UIOrientation.Portrait;
|
||||
PlayerSettings.runInBackground = true;
|
||||
EditorSettings.defaultBehaviorMode = EditorBehaviorMode.Mode2D;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ namespace TKS
|
|||
/// <summary>
|
||||
/// 자동 플레이 실측: 아군도 AI 로 조작해 몇 턴을 돌리며 스크린샷·로그를 남긴다.
|
||||
/// 에디터에서 ProjectSetup.AutoPlayCapture 가 SessionState 로 출력 폴더를 넘기면 생성된다.
|
||||
/// 비활성 에디터 창은 프레임이 느리므로 timeScale 을 올려 실시간 기준으로 빨리 끝낸다.
|
||||
/// </summary>
|
||||
public class AutoPlayDriver : MonoBehaviour
|
||||
{
|
||||
|
|
@ -36,6 +37,8 @@ namespace TKS
|
|||
IEnumerator Start()
|
||||
{
|
||||
Directory.CreateDirectory(OutputDir);
|
||||
Time.timeScale = 4f;
|
||||
Time.maximumDeltaTime = 1f;
|
||||
float t0 = Time.realtimeSinceStartup;
|
||||
while ((BattleManager.Instance == null || BattleManager.Instance.Phase != BattlePhase.PlayerTurn)
|
||||
&& Time.realtimeSinceStartup - t0 < 20f)
|
||||
|
|
@ -50,23 +53,43 @@ namespace TKS
|
|||
Finish(1);
|
||||
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 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);
|
||||
if (first != null)
|
||||
{
|
||||
var tgt = EnemyAI.Decide(first, bm.AliveUnits(Faction.Enemy), 1f, bm.Rng, bm.Board);
|
||||
Vector2 pointer = first.Position - (tgt - first.Position) * 0.5f;
|
||||
bm.Sling.ShowPreview(first, tgt, pointer);
|
||||
var tgt = EnemyAI.Decide(first, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board);
|
||||
if (tgt.HasValue)
|
||||
{
|
||||
Vector2 pointer = first.Position - (tgt.Value - first.Position) * 0.5f;
|
||||
bm.Sling.ShowPreview(first, tgt.Value, pointer);
|
||||
first.View.SetSelected(true);
|
||||
yield return null;
|
||||
yield return Shot(cam, "aim_" + first.DisplayName);
|
||||
first.View.SetSelected(false);
|
||||
bm.Sling.HidePreview();
|
||||
}
|
||||
}
|
||||
|
||||
bool firstActionShot = false;
|
||||
for (int turn = 1; turn <= MaxTurns; turn++)
|
||||
|
|
@ -80,8 +103,9 @@ namespace TKS
|
|||
bm.OnEmptyTap();
|
||||
break;
|
||||
}
|
||||
var target = EnemyAI.Decide(u, bm.AliveUnits(Faction.Enemy), 1f, bm.Rng, bm.Board);
|
||||
if (!bm.RequestPlayerAction(u, target)) break;
|
||||
var target = EnemyAI.Decide(u, bm.AliveUnits(Faction.Enemy), 1f, float.PositiveInfinity, bm.Rng, bm.Board);
|
||||
if (!target.HasValue) break;
|
||||
if (!bm.RequestPlayerAction(u, target.Value)) break;
|
||||
while (bm.Phase == BattlePhase.Resolving) yield return null;
|
||||
Log("turn " + turn + " | " + bm.LastMessage);
|
||||
if (!firstActionShot)
|
||||
|
|
@ -93,11 +117,11 @@ namespace TKS
|
|||
}
|
||||
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);
|
||||
yield return Fx.Wait(0.4f);
|
||||
yield return Shot(cam, "turn" + turn + "_end");
|
||||
if (bm.Phase == BattlePhase.StageClear || bm.Phase == BattlePhase.Defeat) break;
|
||||
}
|
||||
|
||||
File.WriteAllLines(Path.Combine(OutputDir, "autoplay_log.txt"), log.ToArray());
|
||||
Finish(0);
|
||||
}
|
||||
|
||||
|
|
@ -122,10 +146,6 @@ namespace TKS
|
|||
{
|
||||
log.Add(s);
|
||||
Debug.Log("[AutoPlay] " + s);
|
||||
}
|
||||
|
||||
void Finish(int code)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(Path.Combine(OutputDir, "autoplay_log.txt"), log.ToArray());
|
||||
|
|
@ -133,6 +153,12 @@ namespace TKS
|
|||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void Finish(int code)
|
||||
{
|
||||
Log("finish code=" + code);
|
||||
Time.timeScale = 1f;
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.Exit(code);
|
||||
#else
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ namespace TKS
|
|||
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>();
|
||||
}
|
||||
|
||||
|
|
@ -82,8 +84,12 @@ namespace TKS
|
|||
public float finalHitTolerance = 0.15f;
|
||||
public float moveSpeed = 9f;
|
||||
|
||||
[Header("전장")]
|
||||
public Vector2 boardSize = new Vector2(12f, 16f);
|
||||
[Header("전장·카메라 (화면보다 넓은 전장을 스크롤)")]
|
||||
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];
|
||||
|
||||
[Header("아군 영웅 (최대 5)")]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace TKS
|
||||
{
|
||||
/// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·스킬 아이콘(추후)·결과 오버레이.</summary>
|
||||
/// <summary>화면 HUD(uGUI · 코드 생성): 스테이지·턴·유닛 수·안내·선택 유닛 정보·미니맵·스킬 아이콘(추후)·결과 오버레이.</summary>
|
||||
public class BattleHUD : MonoBehaviour
|
||||
{
|
||||
BattleManager battle;
|
||||
Camera uiCam;
|
||||
CameraController camCtrl;
|
||||
Text stageText;
|
||||
Text turnText;
|
||||
Text countText;
|
||||
|
|
@ -19,9 +22,15 @@ namespace TKS
|
|||
Text resultSub;
|
||||
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;
|
||||
uiCam = cam;
|
||||
camCtrl = cc;
|
||||
var canvas = gameObject.AddComponent<Canvas>();
|
||||
canvas.renderMode = RenderMode.ScreenSpaceCamera;
|
||||
canvas.worldCamera = cam;
|
||||
|
|
@ -58,6 +67,16 @@ namespace TKS
|
|||
skillLabel.text = "스킬\n(추후)";
|
||||
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;
|
||||
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);
|
||||
|
|
@ -111,10 +130,28 @@ namespace TKS
|
|||
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()
|
||||
{
|
||||
if (battle == null || battle.Config == null || battle.Config.stages.Length == 0) return;
|
||||
Refresh();
|
||||
RefreshMinimap();
|
||||
}
|
||||
|
||||
void Refresh()
|
||||
|
|
@ -138,7 +175,7 @@ namespace TKS
|
|||
{
|
||||
hintText.text = battle.OnlyBonusMovesRemain()
|
||||
? "추가 이동: 유닛을 당겨 이동 · 빈 곳을 탭하면 건너뜀"
|
||||
: "아군 유닛을 잡고 뒤로 당겼다 놓으면 반대 방향으로 이동·공격";
|
||||
: "유닛을 잡고 뒤로 당겼다 놓기 = 이동·공격 · 빈 곳 드래그 = 전장 이동";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -178,5 +215,55 @@ namespace TKS
|
|||
: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,14 @@ namespace TKS
|
|||
get { return Config.stages[Mathf.Clamp(StageIndex, 0, Config.stages.Length - 1)]; }
|
||||
}
|
||||
|
||||
/// <summary>상태가 바뀔 때(HUD 갱신용).</summary>
|
||||
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()
|
||||
{
|
||||
|
|
@ -72,6 +79,14 @@ namespace TKS
|
|||
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)
|
||||
|
|
@ -129,6 +144,7 @@ namespace TKS
|
|||
{
|
||||
StopAllCoroutines();
|
||||
ClearUnits();
|
||||
Phase = BattlePhase.Setup;
|
||||
StageIndex = Mathf.Clamp(index, 0, Config.stages.Length - 1);
|
||||
var stage = Config.stages[StageIndex];
|
||||
|
||||
|
|
@ -144,9 +160,9 @@ namespace TKS
|
|||
}
|
||||
|
||||
TurnNumber = 1;
|
||||
Phase = BattlePhase.PlayerTurn;
|
||||
LastMessage = stage.title + " — 아군 차례";
|
||||
NotifyChanged();
|
||||
if (StageStarted != null) StageStarted();
|
||||
SetPhase(BattlePhase.PlayerTurn);
|
||||
}
|
||||
|
||||
public void ContinueAfterResult()
|
||||
|
|
@ -171,8 +187,8 @@ namespace TKS
|
|||
public bool RequestPlayerAction(Unit u, Vector2 target)
|
||||
{
|
||||
if (!CanAct(u)) return false;
|
||||
Phase = BattlePhase.Resolving;
|
||||
NotifyChanged();
|
||||
SetPhase(BattlePhase.Resolving);
|
||||
if (UnitActing != null) UnitActing(u);
|
||||
StartCoroutine(PlayerActionRoutine(u, target));
|
||||
return true;
|
||||
}
|
||||
|
|
@ -196,8 +212,7 @@ namespace TKS
|
|||
}
|
||||
else
|
||||
{
|
||||
Phase = BattlePhase.PlayerTurn;
|
||||
NotifyChanged();
|
||||
SetPhase(BattlePhase.PlayerTurn);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,8 +246,7 @@ namespace TKS
|
|||
{
|
||||
if (u.BonusMoveAvailable) u.MarkActed();
|
||||
}
|
||||
Phase = BattlePhase.Resolving;
|
||||
NotifyChanged();
|
||||
SetPhase(BattlePhase.Resolving);
|
||||
StartCoroutine(EnemyTurnRoutine());
|
||||
}
|
||||
|
||||
|
|
@ -245,14 +259,16 @@ namespace TKS
|
|||
|
||||
IEnumerator EnemyTurnRoutine()
|
||||
{
|
||||
Phase = BattlePhase.EnemyTurn;
|
||||
SetPhase(BattlePhase.EnemyTurn);
|
||||
LastMessage = "적군 차례";
|
||||
NotifyChanged();
|
||||
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);
|
||||
float accuracy = Stage.aiAccuracy;
|
||||
float aggro = Stage.aiAggroRange;
|
||||
int acted = 0;
|
||||
for (int i = 0; i < enemies.Count; i++)
|
||||
{
|
||||
var e = enemies[i];
|
||||
|
|
@ -260,21 +276,26 @@ namespace TKS
|
|||
var players = AliveUnits(Faction.Player);
|
||||
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();
|
||||
yield return Resolver.Resolve(e, target, false, o);
|
||||
yield return Resolver.Resolve(e, target.Value, false, o);
|
||||
yield return ApplyOverwatch(e, o.Path);
|
||||
LastMessage = Describe(e, o, false);
|
||||
NotifyChanged();
|
||||
acted++;
|
||||
if (CheckBattleEnd()) yield break;
|
||||
yield return Fx.Wait(0.25f);
|
||||
}
|
||||
if (acted == 0) yield return Fx.Wait(0.3f);
|
||||
|
||||
foreach (var u in AliveUnits(Faction.Player)) u.ResetForNewTurn();
|
||||
TurnNumber++;
|
||||
Phase = BattlePhase.PlayerTurn;
|
||||
LastMessage = TurnNumber + "번째 턴 — 아군 차례";
|
||||
NotifyChanged();
|
||||
LastMessage = TurnNumber + "번째 턴 — 아군 차례" + (acted == 0 ? " (적은 교전 범위 밖에서 대기)" : "");
|
||||
SetPhase(BattlePhase.PlayerTurn);
|
||||
}
|
||||
|
||||
/// <summary>창병 대기: 상대 유닛이 지나간 경로가 대기 중인 창병 반경을 스치면 피해(창병마다 1회).</summary>
|
||||
|
|
@ -314,16 +335,14 @@ namespace TKS
|
|||
{
|
||||
if (AliveCount(Faction.Enemy) == 0)
|
||||
{
|
||||
Phase = BattlePhase.StageClear;
|
||||
LastMessage = "승리 — 전장의 적을 모두 처치했습니다";
|
||||
NotifyChanged();
|
||||
SetPhase(BattlePhase.StageClear);
|
||||
return true;
|
||||
}
|
||||
if (AliveCount(Faction.Player) == 0)
|
||||
{
|
||||
Phase = BattlePhase.Defeat;
|
||||
LastMessage = "패배 — 아군이 전멸했습니다";
|
||||
NotifyChanged();
|
||||
SetPhase(BattlePhase.Defeat);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -338,7 +357,7 @@ namespace TKS
|
|||
|
||||
public void ForcePhase(BattlePhase p)
|
||||
{
|
||||
Phase = p;
|
||||
SetPhase(p);
|
||||
NotifyChanged();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace TKS
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>전장(바닥·테두리·4면 벽 콜라이더).</summary>
|
||||
/// <summary>전장(바닥·격자·테두리·4면 벽 콜라이더). 화면보다 넓어 카메라로 스크롤한다.</summary>
|
||||
public class Board : MonoBehaviour
|
||||
{
|
||||
public Vector2 Size { get; private set; }
|
||||
|
|
@ -31,12 +31,28 @@ namespace TKS
|
|||
enemyHalf.transform.localScale = new Vector3(w, h * 0.5f, 1f);
|
||||
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);
|
||||
mid.transform.localScale = new Vector3(w, 0.04f, 1f);
|
||||
// 격자(4 단위) — 스크롤 체감용
|
||||
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);
|
||||
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("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);
|
||||
|
|
@ -52,7 +68,7 @@ namespace TKS
|
|||
|
||||
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.localScale = new Vector3(size.x, size.y, 1f);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e83ae9280a929b244832db516173bdf0
|
||||
|
|
@ -3,10 +3,10 @@ using UnityEngine;
|
|||
|
||||
namespace TKS
|
||||
{
|
||||
/// <summary>적군 슬링샷 결정: 가장 가까운 아군을 향해 당기고, 정확도에 따라 흔들린다.</summary>
|
||||
/// <summary>적군 슬링샷 결정: 교전 범위 안의 가장 가까운 아군을 향해 당기고, 정확도에 따라 흔들린다. 범위 밖이면 대기(null).</summary>
|
||||
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;
|
||||
float best = float.MaxValue;
|
||||
|
|
@ -21,7 +21,8 @@ namespace TKS
|
|||
nearest = t;
|
||||
}
|
||||
}
|
||||
if (nearest == null) return e.Position;
|
||||
if (nearest == null) return null;
|
||||
if (best > aggroRange) return null; // 넓은 전장: 교전 범위 밖이면 자리 지킴
|
||||
|
||||
Vector2 toT = nearest.Position - e.Position;
|
||||
float dist = toT.magnitude;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ namespace TKS
|
|||
public int StartStageIndex = 0;
|
||||
|
||||
public BattleManager Battle { get; private set; }
|
||||
public CameraController CameraCtrl { get; private set; }
|
||||
|
||||
void Awake()
|
||||
{
|
||||
|
|
@ -33,54 +34,47 @@ namespace TKS
|
|||
cam.transform.position = new Vector3(0f, 0f, -10f);
|
||||
cam.nearClipPlane = 0.1f;
|
||||
cam.farClipPlane = 100f;
|
||||
var fit = cam.GetComponent<CameraFitter>();
|
||||
if (fit == null) fit = cam.gameObject.AddComponent<CameraFitter>();
|
||||
|
||||
var board = new GameObject("Board").AddComponent<Board>();
|
||||
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.Configure(Config, board);
|
||||
|
||||
var hud = new GameObject("HUD").AddComponent<BattleHUD>();
|
||||
hud.Build(Battle, cam);
|
||||
hud.Build(Battle, cam, CameraCtrl);
|
||||
Battle.Hud = hud;
|
||||
|
||||
var input = new GameObject("SlingshotInput").AddComponent<SlingshotInput>();
|
||||
input.Init(Battle, cam, hud);
|
||||
input.Init(Battle, cam, hud, CameraCtrl);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>화면 비율이 달라도 전장 전체가 보이도록 직교 카메라 크기를 맞춘다.</summary>
|
||||
public class CameraFitter : MonoBehaviour
|
||||
/// <summary>아군 무게중심에서 적 쪽으로 조금 올린 지점.</summary>
|
||||
static Vector2 PlayerFocusPoint(BattleManager battle)
|
||||
{
|
||||
public Board Board;
|
||||
public float Margin = 0.5f;
|
||||
Camera cam;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
cam = GetComponent<Camera>();
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
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);
|
||||
var players = battle.AliveUnits(Faction.Player);
|
||||
if (players.Count == 0) return Vector2.zero;
|
||||
Vector2 sum = Vector2.zero;
|
||||
foreach (var p in players) sum += p.Position;
|
||||
return sum / players.Count + new Vector2(0f, battle.Config.cameraViewHeight * 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ namespace TKS
|
|||
{
|
||||
/// <summary>
|
||||
/// 슬링샷 조작: 아군 유닛을 잡고(누름) 뒤로 당긴 뒤(드래그) 놓으면(드롭) 반대 방향으로 이동·공격.
|
||||
/// 빈 곳 드래그 = 전장 스크롤 · 휠 = 줌 · 미니맵 탭 = 그 지점으로 이동.
|
||||
/// 버튼 없음. 결과 화면에서는 탭으로 계속. 추가 이동만 남았을 때 빈 곳 탭 = 건너뛰기.
|
||||
/// </summary>
|
||||
public class SlingshotInput : MonoBehaviour
|
||||
|
|
@ -12,12 +13,15 @@ namespace TKS
|
|||
BattleManager battle;
|
||||
Camera cam;
|
||||
BattleHUD hud;
|
||||
CameraController camCtrl;
|
||||
|
||||
Unit selected;
|
||||
bool dragging;
|
||||
bool panning;
|
||||
bool emptyPress;
|
||||
bool previewValid;
|
||||
Vector2 pressWorld;
|
||||
Vector2 lastScreen;
|
||||
float panPixels;
|
||||
Vector2 currentTarget;
|
||||
|
||||
LineRenderer aimLine;
|
||||
|
|
@ -30,11 +34,17 @@ namespace TKS
|
|||
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;
|
||||
cam = c;
|
||||
hud = h;
|
||||
camCtrl = cc;
|
||||
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);
|
||||
targetRing = ProceduralSprites.NewSprite(transform, "TargetRing", ProceduralSprites.Ring, Color.white, 0.7f, 11);
|
||||
|
|
@ -59,9 +69,9 @@ namespace TKS
|
|||
return lr;
|
||||
}
|
||||
|
||||
Vector2 PointerWorld()
|
||||
Vector2 ScreenToWorld(Vector2 screen)
|
||||
{
|
||||
Vector3 p = Input.mousePosition;
|
||||
Vector3 p = screen;
|
||||
p.z = -cam.transform.position.z;
|
||||
return cam.ScreenToWorldPoint(p);
|
||||
}
|
||||
|
|
@ -76,16 +86,36 @@ namespace TKS
|
|||
return;
|
||||
}
|
||||
|
||||
if (Input.GetMouseButtonDown(0)) OnPress(PointerWorld());
|
||||
else if (dragging && Input.GetMouseButton(0)) OnDrag(PointerWorld());
|
||||
if (Input.GetMouseButtonUp(0)) OnRelease(PointerWorld());
|
||||
Vector2 screen = Input.mousePosition;
|
||||
if (Input.GetMouseButtonDown(0)) OnPress(screen);
|
||||
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;
|
||||
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);
|
||||
if (u == null) u = battle.UnitNear(w, null);
|
||||
Select(u);
|
||||
|
|
@ -93,10 +123,21 @@ namespace TKS
|
|||
{
|
||||
dragging = true;
|
||||
}
|
||||
else
|
||||
else if (u == null)
|
||||
{
|
||||
dragging = false;
|
||||
emptyPress = (u == null);
|
||||
panning = true;
|
||||
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);
|
||||
}
|
||||
|
||||
void OnRelease(Vector2 w)
|
||||
void OnRelease(Vector2 screen)
|
||||
{
|
||||
if (dragging && selected != null && previewValid && battle.CanAct(selected))
|
||||
{
|
||||
|
|
@ -128,7 +169,7 @@ namespace TKS
|
|||
Select(null);
|
||||
battle.RequestPlayerAction(u, t);
|
||||
}
|
||||
else if (emptyPress && Vector2.Distance(w, pressWorld) < 0.3f)
|
||||
else if (emptyPress && panPixels <= TapPixels)
|
||||
{
|
||||
battle.OnEmptyTap();
|
||||
}
|
||||
|
|
@ -137,6 +178,7 @@ namespace TKS
|
|||
HidePreview();
|
||||
}
|
||||
dragging = false;
|
||||
panning = false;
|
||||
emptyPress = false;
|
||||
previewValid = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,10 +31,11 @@ namespace TKS
|
|||
baseColor = u.Faction == Faction.Player ? PlayerColor : EnemyColor;
|
||||
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);
|
||||
owRing.transform.localScale = Vector3.one;
|
||||
overwatchArea.gameObject.SetActive(false);
|
||||
|
||||
if (u.IsBoss)
|
||||
{
|
||||
|
|
@ -79,7 +80,8 @@ namespace TKS
|
|||
if (flash == null) body.color = c;
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +125,7 @@ namespace TKS
|
|||
{
|
||||
selectRing.enabled = false;
|
||||
highlightRing.enabled = false;
|
||||
overwatchArea.enabled = false;
|
||||
overwatchArea.gameObject.SetActive(false);
|
||||
float t = 0f;
|
||||
const float dur = 0.45f;
|
||||
Vector3 s0 = transform.localScale;
|
||||
|
|
|
|||
|
|
@ -220,5 +220,35 @@ namespace TKS.Tests
|
|||
Assert.AreEqual(BattlePhase.StageClear, bm.Phase);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ PlayerSettings:
|
|||
androidApplicationEntry: 2
|
||||
defaultIsNativeResolution: 1
|
||||
macRetinaSupport: 1
|
||||
runInBackground: 0
|
||||
runInBackground: 1
|
||||
muteOtherAudioSources: 0
|
||||
Prepare IOS For Recording: 0
|
||||
Force IOS Speakers When Recording: 0
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ Unity 6000.3.23f1 · 2D · 도형 플레이스홀더(아트 리소스는 별도
|
|||
|
||||
- 아군 유닛을 누른 채 **뒤로 당겼다 놓으면** 반대 방향으로 날아간다(슬링샷). 하얀 선 = 날아갈 경로, 끝의 링 = 최종 목표점. 링이 주황색이면 목표점이 적 위에 있다(명중).
|
||||
- 이동과 공격은 하나의 조작이다. 경로 위에 적이 있으면 병종 규칙대로 공격한다. 주황 링이 켜진 적 = 이번 조작으로 공격하게 될 적.
|
||||
- 전장(20×32)은 화면보다 넓다. **빈 곳을 드래그하면 스크롤**, 마우스 휠 = 줌, 오른쪽 위 **미니맵을 탭하면 그 지점으로 이동**. 행동 중인 유닛은 카메라가 따라간다.
|
||||
- 적은 교전 범위(스테이지별 11~12) 안에 아군이 들어와야 움직인다. 멀리 있는 부대는 자리를 지키므로 부대 단위로 각개격파할 수 있다.
|
||||
- 아군 전원이 행동하면 자동으로 적군 턴이 진행된다. 보병 추가 이동만 남았을 때 빈 곳을 탭하면 건너뛴다.
|
||||
- 승리/패배 화면은 탭으로 다음 전투·재도전.
|
||||
- 스킬 아이콘은 자리만(초기 단계 미구현).
|
||||
|
|
@ -35,7 +37,7 @@ Unity 6000.3.23f1 · 2D · 도형 플레이스홀더(아트 리소스는 별도
|
|||
## CLI
|
||||
|
||||
```
|
||||
# 컴파일 + 설정 자산·씬 생성
|
||||
# 컴파일 + 설정 자산·씬 생성 (BuildFresh = 기본값 재시드까지)
|
||||
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/Tests/PlayMode/` — 병종 규칙·턴 흐름·승리 판정 테스트
|
||||
|
|
|
|||
Loading…
Reference in New Issue