feat: 미션 판정 (완료/채점) — 입고 게임 최소 루프 완성

- MissionManager: 슬롯 감시 → 전부 정답 시 완료, 점수/오답/시간 표시, R키 재시작
- PutawaySlot: Changed 이벤트로 상태 변화 알림
- MissionSetup: Mission 오브젝트 + MissionManager 추가

이동→집기→적치→정답판정→완료/채점 한 판 루프 완성.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jklee
2026-06-14 20:14:51 +09:00
parent aa302b1d67
commit f230b438ed
5 changed files with 139 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.InputSystem;
namespace SinoWms.Gameplay
{
/// <summary>
/// 씬의 모든 적치 슬롯을 감시해 미션 진행/완료를 판정한다.
/// 모든 슬롯이 정답으로 채워지면 완료 처리하고 점수(정답·오답·시간)를 표시한다.
/// </summary>
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<PutawaySlot>(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);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 27f48d56c8f68491ba5b07b4887165cc
+4
View File
@@ -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);
/// <summary>적치/제거 등 상태가 바뀌면 발생(미션 판정용).</summary>
public event System.Action<PutawaySlot> 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);
}
}
}