From e01987c1d8152b87ed85ccb48cfd60d9b882a6af Mon Sep 17 00:00:00 2001 From: Sehyeon Date: Mon, 12 May 2025 14:49:57 +0900 Subject: [PATCH] =?UTF-8?q?DEG-137=20[Feat]=20=EC=B6=9C=EA=B7=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/KSH/PlayerStats.cs | 106 +++++++++++++++++- Assets/KSH/SpeechBubbleFollower.cs | 75 +++++++++++++ Assets/KSH/SpeechBubbleFollower.cs.meta | 11 ++ .../DailyRoutine/InteractionController.cs | 13 +++ .../UI/InteractionAnimationPanelController.cs | 3 + Assets/Resources/Dialogues/dialogue.json | 49 ++++++++ Assets/Resources/Prefabs/MessagePanel.prefab | 3 + .../Prefabs/MessagePanel.prefab.meta | 7 ++ .../Prefabs/Panels/PopupPanel.prefab | 4 +- .../Common/Dialogue/ChatWindowController.cs | 6 +- .../Common/Dialogue/FairyDialogueManager.cs | 4 + Assets/Scripts/Common/GameManager.cs | 6 +- .../Scripts/Common/GameUtility/EndingLogic.cs | 13 ++- 13 files changed, 288 insertions(+), 12 deletions(-) create mode 100644 Assets/KSH/SpeechBubbleFollower.cs create mode 100644 Assets/KSH/SpeechBubbleFollower.cs.meta create mode 100644 Assets/Resources/Prefabs/MessagePanel.prefab create mode 100644 Assets/Resources/Prefabs/MessagePanel.prefab.meta diff --git a/Assets/KSH/PlayerStats.cs b/Assets/KSH/PlayerStats.cs index da3d4a9d..f0e92911 100644 --- a/Assets/KSH/PlayerStats.cs +++ b/Assets/KSH/PlayerStats.cs @@ -29,7 +29,7 @@ public class PlayerStats : MonoBehaviour public event Action OnDayEnded; public event Action Exhaustion; // 탈진 - public event Action Overslept; // 결근(늦잠) + public event Action Overslept; // 늦잠 public event Action ZeroReputation; // 평판 0 이벤트 public event Action OnStatsChanged; // 스탯 변경 이벤트 public event Action OnWorked; // 퇴근 이벤트 (출근 이후 집에 돌아올 시간에 발생) @@ -37,6 +37,18 @@ public class PlayerStats : MonoBehaviour private float previousAddHealth = 0f; public static PlayerStats Instance; + + // 결근 이벤트 관련 변수 + private bool _hasWorkedToday = false; + private bool _hasCheckedAbsenceToday = false; // 결근 체크, 하루에 결근 여러 번 체크 안하기 위함 + public event Action OnAbsent; // 결근 + + // 말풍선 + private GameObject messagePanelInstance; + private SpeechBubbleFollower speechBubbleFollower; + private bool isActiveBubble; + private bool hasShownBubbleToday; // 하루에 말풍선 하나만 표시하기 + private void Awake() { if (Instance == null) @@ -59,8 +71,63 @@ public class PlayerStats : MonoBehaviour { _valueByAction = new ValueByAction(); _valueByAction.Initialize(); // 값 초기화 + + LoadMessagePanel(); + CheckBubble(); } + #region 말풍선(Bubble) 관련 + + private void LoadMessagePanel() + { + GameObject messagePanelPrefab = Resources.Load("Prefabs/MessagePanel"); + + if (messagePanelPrefab != null) + { + Canvas canvas = FindObjectOfType(); + + messagePanelInstance = Instantiate(messagePanelPrefab, canvas.transform); + speechBubbleFollower = messagePanelInstance.GetComponent(); + + if (speechBubbleFollower != null) + { + isActiveBubble = false; + hasShownBubbleToday = false; + speechBubbleFollower.HideMessage(); + } + } + } + + private void CheckBubble() + { + if (isActiveBubble) + { + isActiveBubble = false; + HideBubble(); + } + + if (TimeStat >= 8.0f && TimeStat < 9.0f && !isActiveBubble && !hasShownBubbleToday) + { + hasShownBubbleToday = true; + isActiveBubble = true; + ShowBubble(); + } + } + + public void ShowBubble() + { + if(isActiveBubble) + speechBubbleFollower.ShowMessage(); + } + + public void HideBubble() + { + if(!isActiveBubble) + speechBubbleFollower.HideMessage(); + } + + #endregion + // 현재 체력으로 해당 행동이 가능한 지 확인 public bool CanPerformByHealth(ActionType actionType) { @@ -69,6 +136,23 @@ public class PlayerStats : MonoBehaviour return (HealthStat >= (effect.healthChange * -1)); } + // 결근 체크 + public void CheckAbsent() + { + if (_hasWorkedToday || _hasCheckedAbsenceToday) + return; + + // 9시가 지났는데 출근하지 않은 경우 + if (TimeStat >= 9.0f && !_hasWorkedToday) + { + _hasCheckedAbsenceToday = true; // 결근 체크 완료 표시 + OnAbsent?.Invoke(); + + PerformAction(ActionType.Absence); // 평판 -3 + Debug.Log("결근 처리: 평판 감소" + ReputationStat); + } + } + // 행동 처리 메서드 public void PerformAction(ActionType actionType) { @@ -86,6 +170,7 @@ public class PlayerStats : MonoBehaviour // 스탯 - 시간이 변경된 이후 퇴근 이벤트 발생 if (actionType == ActionType.Work) { + _hasWorkedToday = true; OnWorked?.Invoke(); } } @@ -156,6 +241,11 @@ public class PlayerStats : MonoBehaviour // 하루가 실제로 종료된 경우에만 이벤트 발생 if (isDayEnded) { + // 결근 관련 변수 초기화 + _hasWorkedToday = false; + _hasCheckedAbsenceToday = false; + hasShownBubbleToday = false; + OnDayEnded?.Invoke(); } } @@ -178,11 +268,13 @@ public class PlayerStats : MonoBehaviour public void ModifyTime(float time, ActionType actionType) { TimeStat += time; - + if (TimeStat >= _gameConstants.maxTime) { EndDay(time, actionType); } + + CheckBubble(); } public void ModifyHealth(float health) @@ -211,11 +303,17 @@ public class PlayerStats : MonoBehaviour public void ModifyReputation(float reputation) { // float 연산 시 계산 오차가 발생할 수도 있기에 소수점 두 번째에서 반올림하도록 처리 - ReputationStat = Mathf.Round((ReputationStat + reputation) * 100f) / 100f; + if(ReputationStat > 0) + { + ReputationStat = Mathf.Round((ReputationStat + reputation) * 100f) / 100f; + } + else + { + ReputationStat = 0f; + } if (ReputationStat <= 0) { - Debug.Log("당신의 평판은 0입니다..;"); ZeroReputation?.Invoke(); ReputationStat = 0.0f; } diff --git a/Assets/KSH/SpeechBubbleFollower.cs b/Assets/KSH/SpeechBubbleFollower.cs new file mode 100644 index 00000000..2777add0 --- /dev/null +++ b/Assets/KSH/SpeechBubbleFollower.cs @@ -0,0 +1,75 @@ +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +public class SpeechBubbleFollower : MonoBehaviour +{ + [SerializeField] private Transform playerTransform; + [SerializeField] private TMP_Text bubbleText; + + private Vector3 offset = new Vector3(200f, 250f, 0); + + private Camera mainCamera; + private RectTransform rectTransform; + private CanvasGroup canvasGroup; + + // 랜덤 메시지 + private string[] workReminderMessages = new string[] + { + "8시.. 출근하자.", + "출근...해야 하나.", + "회사가 날 기다린다." + }; + + private void Awake() + { + rectTransform = GetComponent(); + canvasGroup = GetComponent(); + + if (canvasGroup == null) + canvasGroup = gameObject.AddComponent(); + + gameObject.SetActive(false); + } + + private void Start() + { + mainCamera = Camera.main; + + if (playerTransform == null) + { + playerTransform = GameObject.FindGameObjectWithTag("Player").transform; + } + } + + private void LateUpdate() + { + if (!gameObject.activeInHierarchy || playerTransform == null) + return; + + // 플레이어 위치를 스크린 좌표로 변환 + Vector3 screenPosition = mainCamera.WorldToScreenPoint(playerTransform.position); + + // 화면 상의 위치만 사용 (z값은 무시) + screenPosition.z = 0; + + // 고정된 오프셋 적용 + rectTransform.position = screenPosition + offset; + } + + public void ShowMessage() // 랜덤 텍스트 표시 + { + string message = workReminderMessages[Random.Range(0, workReminderMessages.Length)]; + + if (bubbleText != null) + bubbleText.text = message; + + gameObject.SetActive(true); + canvasGroup.alpha = 1f; + } + + public void HideMessage() + { + gameObject.SetActive(false); + } +} \ No newline at end of file diff --git a/Assets/KSH/SpeechBubbleFollower.cs.meta b/Assets/KSH/SpeechBubbleFollower.cs.meta new file mode 100644 index 00000000..e74bd9d7 --- /dev/null +++ b/Assets/KSH/SpeechBubbleFollower.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91cfaa5ec19c50b41ac2d6c542b51cd1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/LIN/Scripts/DailyRoutine/InteractionController.cs b/Assets/LIN/Scripts/DailyRoutine/InteractionController.cs index 1814305a..be753f8e 100644 --- a/Assets/LIN/Scripts/DailyRoutine/InteractionController.cs +++ b/Assets/LIN/Scripts/DailyRoutine/InteractionController.cs @@ -61,6 +61,17 @@ public class InteractionController : MonoBehaviour { if (PlayerStats.Instance.CanPerformByHealth(interactionType)) { + if (interactionType == ActionType.Work) + { + if (!PlayerStats.Instance.CanWork()) // 출근 가능한 시간이 아닐 경우 + { + // 텍스트 출력 X ?? + Debug.Log("Can't work"); + housingCanvasController.interactionTextsController.ActiveTexts( "출근 가능한 시간이 아닙니다!"); + return; + } + } + PlayerStats.Instance.PerformAction(interactionType); if (interactionType == ActionType.Dungeon) @@ -96,6 +107,8 @@ public class InteractionController : MonoBehaviour housingCanvasController.ShowSuddenEventPanel("부장님이 퇴근을 안하셔.. 야근할까?", () => { //Todo: 컷씬과 스테이터스 변경 + // 체력상 가능한지 확인 이후 행동 수행 + housingCanvasController.HideSuddenEventPanel(); }); break; diff --git a/Assets/LIN/Scripts/UI/InteractionAnimationPanelController.cs b/Assets/LIN/Scripts/UI/InteractionAnimationPanelController.cs index d8962147..228c23d1 100644 --- a/Assets/LIN/Scripts/UI/InteractionAnimationPanelController.cs +++ b/Assets/LIN/Scripts/UI/InteractionAnimationPanelController.cs @@ -107,6 +107,9 @@ public class InteractionAnimationPanelController : MonoBehaviour StopCoroutine(_autoHideCoroutine); _autoHideCoroutine = null; } + + // 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함 + PlayerStats.Instance.CheckAbsent(); } public void TutorialSleepAnimation() diff --git a/Assets/Resources/Dialogues/dialogue.json b/Assets/Resources/Dialogues/dialogue.json index 915376fd..ab8b7e91 100644 --- a/Assets/Resources/Dialogues/dialogue.json +++ b/Assets/Resources/Dialogues/dialogue.json @@ -146,6 +146,55 @@ "text": "... GameManager.Instance.ShowCredit(GamePhase.End);", "nextId": "", "phase": "end" + }, + { + "id": "fairy_zero_1", + "name": "냉장고 요정", + "text": "...", + "nextId": "player_zero_1", + "phase": "zero" + }, + { + "id": "player_zero_1", + "name": "주인공", + "text": "...", + "nextId": "fairy_zero_2", + "phase": "zero" + }, + { + "id": "fairy_zero_2", + "name": "냉장고 요정", + "text": "평판이... 0?", + "nextId": "player_zero_2", + "phase": "zero" + }, + { + "id": "player_zero_2", + "name": "주인공", + "text": "...! (회사에서 연락이 온다.)", + "nextId": "fairy_zero_3", + "phase": "zero" + }, + { + "id": "fairy_zero_3", + "name": "회사", + "text": "당신은 해고되었습니다.", + "nextId": "player_zero_3", + "phase": "zero" + }, + { + "id": "player_zero_3", + "name": "주인공", + "text": "내가... 해고? 내가? \n 이럴.. 이럴리가 없어!!", + "nextId": "fairy_zero_4", + "phase": "zero" + }, + { + "id": "fairy_zero_4", + "name": " ", + "text": "그 날 서울시 어느 동네에서, 한 34세의 남성의 절규 소리가 울려퍼졌다.", + "nextId": "", + "phase": "zero" } ] } \ No newline at end of file diff --git a/Assets/Resources/Prefabs/MessagePanel.prefab b/Assets/Resources/Prefabs/MessagePanel.prefab new file mode 100644 index 00000000..9ead4929 --- /dev/null +++ b/Assets/Resources/Prefabs/MessagePanel.prefab @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0001dccbde19204bdec40f7f14b3b0d39a39118757ff885b750fc00f34c402a9 +size 8961 diff --git a/Assets/Resources/Prefabs/MessagePanel.prefab.meta b/Assets/Resources/Prefabs/MessagePanel.prefab.meta new file mode 100644 index 00000000..a7e52caf --- /dev/null +++ b/Assets/Resources/Prefabs/MessagePanel.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 45ae814acb957c54785f24aefe6843e8 +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Prefabs/Panels/PopupPanel.prefab b/Assets/Resources/Prefabs/Panels/PopupPanel.prefab index 5fcf8a2a..69b53270 100644 --- a/Assets/Resources/Prefabs/Panels/PopupPanel.prefab +++ b/Assets/Resources/Prefabs/Panels/PopupPanel.prefab @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2986b655092c6503fbf0a724ca144353cd8dbd5d1cee38a59720d8109b12e828 -size 27212 +oid sha256:ff86438641b3f2fb386048b0cd6302b9a0d64be13160ab38c40df1981de6ca0e +size 27192 diff --git a/Assets/Scripts/Common/Dialogue/ChatWindowController.cs b/Assets/Scripts/Common/Dialogue/ChatWindowController.cs index e1b808d8..428cd556 100644 --- a/Assets/Scripts/Common/Dialogue/ChatWindowController.cs +++ b/Assets/Scripts/Common/Dialogue/ChatWindowController.cs @@ -35,7 +35,8 @@ public enum GamePhase // 단계별로 출력되는 대화가 달라짐 { Intro, // 인트로 설명문 Gameplay, // 게임 진행 팁? 등 - End // 엔딩 대화 + End, // 엔딩 대화 + ZeroEnd } public class ChatWindowController : MonoBehaviour, IPointerClickHandler @@ -90,6 +91,7 @@ public class ChatWindowController : MonoBehaviour, IPointerClickHandler if (_inputQueue.Count > 0) { ShowNextDialogue(); + PlayerStats.Instance.HideBubble(); } } @@ -111,6 +113,8 @@ public class ChatWindowController : MonoBehaviour, IPointerClickHandler StopCoroutine(_clickCoroutine); _clickCoroutine = null; } + + PlayerStats.Instance.ShowBubble(); } #endregion diff --git a/Assets/Scripts/Common/Dialogue/FairyDialogueManager.cs b/Assets/Scripts/Common/Dialogue/FairyDialogueManager.cs index 86d5da0b..65fc370e 100644 --- a/Assets/Scripts/Common/Dialogue/FairyDialogueManager.cs +++ b/Assets/Scripts/Common/Dialogue/FairyDialogueManager.cs @@ -86,6 +86,10 @@ public class FairyDialogueManager else if (phase == GamePhase.End) { StartPhaseDialogue("end"); + } + else if (phase == GamePhase.ZeroEnd) + { + StartPhaseDialogue("zero"); } } diff --git a/Assets/Scripts/Common/GameManager.cs b/Assets/Scripts/Common/GameManager.cs index a02bb9d3..c70b9d47 100644 --- a/Assets/Scripts/Common/GameManager.cs +++ b/Assets/Scripts/Common/GameManager.cs @@ -27,7 +27,11 @@ public partial class GameManager : Singleton { // 오디오 초기화 InitializeAudio(); - PlayerStats.Instance.OnDayEnded += AdvanceDay; + + // 이벤트 할당 + PlayerStats.Instance.OnDayEnded += AdvanceDay; // 날짜 변경 + PlayerStats.Instance.ZeroReputation += ZeroReputationEnd; // 평판 0 엔딩 + //패널 매니저 생성 panelManager = Instantiate(Resources.Load("Prefabs/PanelManager")).GetComponent(); } diff --git a/Assets/Scripts/Common/GameUtility/EndingLogic.cs b/Assets/Scripts/Common/GameUtility/EndingLogic.cs index 045b5935..8e77c8a8 100644 --- a/Assets/Scripts/Common/GameUtility/EndingLogic.cs +++ b/Assets/Scripts/Common/GameUtility/EndingLogic.cs @@ -16,9 +16,14 @@ public partial class GameManager public void ClearStage() { - Debug.Log($"스테이지 레벨 {stageLevel}을 클리어 하셨습니다!"); stageLevel++; } + + private void ZeroReputationEnd() + { + // npc와의 대화 출력, Phase = zero + StartNPCDialogue(GamePhase.ZeroEnd); + } // 엔딩 관련 메서드. 7일차에 실행 private void TriggerTimeEnding() @@ -27,10 +32,10 @@ public partial class GameManager StartNPCDialogue(GamePhase.End); // 플레이어 상태에 따라 엔딩 판별 - EndingType endingType = DetermineEnding(); + // EndingType endingType = DetermineEnding(); // 엔딩 타입에 따라 다른 씬이나 UI 표시 - switch (endingType) + /*switch (endingType) { case EndingType.Normal: Debug.Log("던전 공략 성공"); @@ -44,7 +49,7 @@ public partial class GameManager Debug.Log("던전 공략 성공과 훌륭한 평판 작"); break; - } + }*/ } // 던전 스테이지와 평판 수치로 엔딩 판별