From f230b438ed031d4dee0236ae31083e5efeebcbad Mon Sep 17 00:00:00 2001 From: jklee Date: Sun, 14 Jun 2026 20:14:51 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=ED=8C=90=EC=A0=95?= =?UTF-8?q?=20(=EC=99=84=EB=A3=8C/=EC=B1=84=EC=A0=90)=20=E2=80=94=20?= =?UTF-8?q?=EC=9E=85=EA=B3=A0=20=EA=B2=8C=EC=9E=84=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?=EB=A3=A8=ED=94=84=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MissionManager: 슬롯 감시 → 전부 정답 시 완료, 점수/오답/시간 표시, R키 재시작 - PutawaySlot: Changed 이벤트로 상태 변화 알림 - MissionSetup: Mission 오브젝트 + MissionManager 추가 이동→집기→적치→정답판정→완료/채점 한 판 루프 완성. Co-Authored-By: Claude Opus 4.8 (1M context) --- Assets/Scripts/Editor/MissionSetup.cs | 26 +++++ Assets/Scripts/Editor/MissionSetup.cs.meta | 2 + Assets/Scripts/Gameplay/MissionManager.cs | 105 ++++++++++++++++++ .../Scripts/Gameplay/MissionManager.cs.meta | 2 + Assets/Scripts/Gameplay/PutawaySlot.cs | 4 + 5 files changed, 139 insertions(+) create mode 100644 Assets/Scripts/Editor/MissionSetup.cs create mode 100644 Assets/Scripts/Editor/MissionSetup.cs.meta create mode 100644 Assets/Scripts/Gameplay/MissionManager.cs create mode 100644 Assets/Scripts/Gameplay/MissionManager.cs.meta diff --git a/Assets/Scripts/Editor/MissionSetup.cs b/Assets/Scripts/Editor/MissionSetup.cs new file mode 100644 index 0000000..b626b26 --- /dev/null +++ b/Assets/Scripts/Editor/MissionSetup.cs @@ -0,0 +1,26 @@ +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using SinoWms.Gameplay; + +namespace SinoWms.EditorTools +{ + /// + /// 미션 판정 매니저를 씬에 추가한다. 메뉴 SinoWMS/Setup Mission. + /// Play 후 모든 슬롯을 정답으로 채우면 완료 + 점수 표시. + /// + public static class MissionSetup + { + [MenuItem("SinoWMS/Setup Mission")] + public static void Setup() + { + var go = GameObject.Find("Mission") ?? new GameObject("Mission"); + if (go.GetComponent() == null) + go.AddComponent(); + + EditorSceneManager.MarkSceneDirty(go.scene); + Selection.activeGameObject = go; + Debug.Log("[MissionSetup] 완료. Play 후 모든 슬롯을 정답으로 채우면 미션 완료."); + } + } +} diff --git a/Assets/Scripts/Editor/MissionSetup.cs.meta b/Assets/Scripts/Editor/MissionSetup.cs.meta new file mode 100644 index 0000000..bcb5e44 --- /dev/null +++ b/Assets/Scripts/Editor/MissionSetup.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9a8d41437743f4655996e49253cc72ad \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/MissionManager.cs b/Assets/Scripts/Gameplay/MissionManager.cs new file mode 100644 index 0000000..bc29012 --- /dev/null +++ b/Assets/Scripts/Gameplay/MissionManager.cs @@ -0,0 +1,105 @@ +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.InputSystem; + +namespace SinoWms.Gameplay +{ + /// + /// 씬의 모든 적치 슬롯을 감시해 미션 진행/완료를 판정한다. + /// 모든 슬롯이 정답으로 채워지면 완료 처리하고 점수(정답·오답·시간)를 표시한다. + /// + public class MissionManager : MonoBehaviour + { + [SerializeField] private float targetSeconds = 60f; // 시간 보너스 기준 + private const int ScorePerSlot = 100; + private const int MistakePenalty = 25; + + private PutawaySlot[] _slots; + private float _startTime; + private int _mistakes; + private bool _completed; + private int _finalScore; + private float _elapsedAtComplete; + + private GUIStyle _hudStyle; + private GUIStyle _panelStyle; + + private void Start() + { + _slots = Object.FindObjectsByType(FindObjectsSortMode.None); + _startTime = Time.time; + foreach (var s in _slots) + s.Changed += OnSlotChanged; + } + + private void OnDestroy() + { + if (_slots == null) return; + foreach (var s in _slots) + if (s != null) s.Changed -= OnSlotChanged; + } + + private void Update() + { + if (_completed && (Keyboard.current?.rKey.wasPressedThisFrame ?? false)) + SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex); + } + + private void OnSlotChanged(PutawaySlot slot) + { + if (_completed) return; + + // 오답 적치는 누적 집계(교육 감점용) + if (slot.IsOccupied && !slot.IsCorrect) + _mistakes++; + + int correct = 0; + foreach (var s in _slots) + if (s.IsCorrect) correct++; + + if (correct == _slots.Length) + Complete(correct); + } + + private void Complete(int correct) + { + _completed = true; + _elapsedAtComplete = Time.time - _startTime; + + int score = correct * ScorePerSlot - _mistakes * MistakePenalty; + if (_elapsedAtComplete < targetSeconds) + score += Mathf.RoundToInt((1f - _elapsedAtComplete / targetSeconds) * 50f); + _finalScore = Mathf.Max(0, score); + } + + private int CorrectCount() + { + int c = 0; + foreach (var s in _slots) + if (s != null && s.IsCorrect) c++; + return c; + } + + private void OnGUI() + { + _hudStyle ??= new GUIStyle(GUI.skin.box) { fontSize = 18, alignment = TextAnchor.MiddleCenter }; + _panelStyle ??= new GUIStyle(GUI.skin.box) { fontSize = 22, alignment = TextAnchor.MiddleCenter }; + + if (_slots == null || _slots.Length == 0) return; + + if (!_completed) + { + string hud = $"적치 {CorrectCount()}/{_slots.Length} 오답 {_mistakes}"; + GUI.Label(new Rect(Screen.width / 2f - 130f, 64f, 260f, 32f), hud, _hudStyle); + return; + } + + float w = 360f, h = 160f; + var rect = new Rect(Screen.width / 2f - w / 2f, Screen.height / 2f - h / 2f, w, h); + string text = + $"미션 완료!\n\n점수 {_finalScore}점\n" + + $"소요 {_elapsedAtComplete:0}초 · 오답 {_mistakes}건\n\nR: 다시 하기"; + GUI.Label(rect, text, _panelStyle); + } + } +} diff --git a/Assets/Scripts/Gameplay/MissionManager.cs.meta b/Assets/Scripts/Gameplay/MissionManager.cs.meta new file mode 100644 index 0000000..a7d182e --- /dev/null +++ b/Assets/Scripts/Gameplay/MissionManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 27f48d56c8f68491ba5b07b4887165cc \ No newline at end of file diff --git a/Assets/Scripts/Gameplay/PutawaySlot.cs b/Assets/Scripts/Gameplay/PutawaySlot.cs index da6809c..b526623 100644 --- a/Assets/Scripts/Gameplay/PutawaySlot.cs +++ b/Assets/Scripts/Gameplay/PutawaySlot.cs @@ -21,6 +21,9 @@ namespace SinoWms.Gameplay private static readonly Color Correct = new Color(0.3f, 0.8f, 0.4f); private static readonly Color Wrong = new Color(0.85f, 0.3f, 0.3f); + /// 적치/제거 등 상태가 바뀌면 발생(미션 판정용). + public event System.Action Changed; + public Carryable Occupant { get; private set; } public bool IsOccupied => Occupant != null; public bool IsCorrect => @@ -55,6 +58,7 @@ namespace SinoWms.Gameplay { if (pad == null) return; pad.material.color = !IsOccupied ? Empty : (IsCorrect ? Correct : Wrong); + Changed?.Invoke(this); } } }