Compare commits

...

4 Commits

Author SHA1 Message Date
Sehyeon
f666ea85ac git conflict resolve 2025-05-13 09:30:56 +09:00
Sehyeon
82a36ac03d DEG-137 [Fix] 출근 시간에 던전 갈 수 있도록 수정 2025-05-12 16:36:00 +09:00
Sehyeon
5041233ba7 DEG-137 [Feat] 말풍선 최종 2025-05-12 16:34:27 +09:00
Sehyeon
e01987c1d8 DEG-137 [Feat] 출근 수정 2025-05-12 14:49:57 +09:00
13 changed files with 384 additions and 32 deletions

View File

@ -2,6 +2,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using Random = UnityEngine.Random;
public class PlayerStats : MonoBehaviour
@ -29,7 +30,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<StatsChangeData> OnStatsChanged; // 스탯 변경 이벤트
public event Action OnWorked; // 퇴근 이벤트 (출근 이후 집에 돌아올 시간에 발생)
@ -37,6 +38,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 +72,89 @@ public class PlayerStats : MonoBehaviour
{
_valueByAction = new ValueByAction();
_valueByAction.Initialize(); // 값 초기화
LoadMessagePanel();
CheckBubble();
SceneManager.sceneLoaded += OnSceneLoaded; // 씬 전환 이벤트
}
#region (Bubble)
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 새 씬에서 메시지 패널 다시 로드
LoadMessagePanel();
CheckBubble();
}
// OnDestroy에서 이벤트 구독 해제
private void OnDestroy()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
private void LoadMessagePanel()
{
if (messagePanelInstance != null) // 기존 패널 파괴
{
Destroy(messagePanelInstance);
messagePanelInstance = null;
}
GameObject messagePanelPrefab = Resources.Load<GameObject>("Prefabs/MessagePanel");
if (messagePanelPrefab != null)
{
Canvas canvas = FindObjectOfType<Canvas>();
messagePanelInstance = Instantiate(messagePanelPrefab, canvas.transform);
speechBubbleFollower = messagePanelInstance.GetComponent<SpeechBubbleFollower>();
speechBubbleFollower.SetPlayerTransform();
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()
{
speechBubbleFollower.HideMessage();
}
public void ShowAndHideBubble(string text)
{
speechBubbleFollower.ShowAndHide(text);
}
#endregion
// 현재 체력으로 해당 행동이 가능한 지 확인
public bool CanPerformByHealth(ActionType actionType)
{
@ -69,6 +163,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 +197,7 @@ public class PlayerStats : MonoBehaviour
// 스탯 - 시간이 변경된 이후 퇴근 이벤트 발생
if (actionType == ActionType.Work)
{
_hasWorkedToday = true;
OnWorked?.Invoke();
}
}
@ -156,6 +268,11 @@ public class PlayerStats : MonoBehaviour
// 하루가 실제로 종료된 경우에만 이벤트 발생
if (isDayEnded)
{
// 결근 관련 변수 초기화
_hasWorkedToday = false;
_hasCheckedAbsenceToday = false;
hasShownBubbleToday = false;
OnDayEnded?.Invoke();
}
}
@ -174,6 +291,8 @@ public class PlayerStats : MonoBehaviour
}
}
#region Modify Stats
// 행동에 따른 내부 스탯 변경 메서드
public void ModifyTime(float time, ActionType actionType)
{
@ -183,6 +302,8 @@ public class PlayerStats : MonoBehaviour
{
EndDay(time, actionType);
}
CheckBubble();
}
public void ModifyHealth(float health)
@ -211,11 +332,17 @@ public class PlayerStats : MonoBehaviour
public void ModifyReputation(float reputation)
{
// float 연산 시 계산 오차가 발생할 수도 있기에 소수점 두 번째에서 반올림하도록 처리
if(ReputationStat > 0)
{
ReputationStat = Mathf.Round((ReputationStat + reputation) * 100f) / 100f;
}
else
{
ReputationStat = 0f;
}
if (ReputationStat <= 0)
{
Debug.Log("당신의 평판은 0입니다..;");
ZeroReputation?.Invoke();
ReputationStat = 0.0f;
}
@ -225,4 +352,6 @@ public class PlayerStats : MonoBehaviour
ReputationStat = _gameConstants.maxReputation;
}
}
#endregion
}

View File

@ -0,0 +1,120 @@
using System.Collections;
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 float minDistance = 3f;
private float maxDistance = 8f;
private float minOffsetScale = 0.7f;
private Coroutine hideCoroutine; // 자동 숨김용 코루틴
// 랜덤 메시지
private string[] workReminderMessages = new string[]
{
"8시.. 출근하자.",
"출근...해야 하나.",
"회사가 기다린다."
};
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
gameObject.SetActive(false);
}
private void Start()
{
mainCamera = Camera.main;
SetPlayerTransform();
}
public void SetPlayerTransform()
{
if (playerTransform == null)
{
playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
}
}
private void LateUpdate()
{
if (!gameObject.activeInHierarchy || playerTransform == null)
return;
// Z축 거리 계산
float zDistance = Mathf.Abs(mainCamera.transform.position.z - playerTransform.position.z);
// 거리에 따른 오프셋 비율 계산 (멀어질수록 작아짐)
float normalizedDistance = Mathf.Clamp01((zDistance - minDistance) / (maxDistance - minDistance));
float offsetScale = Mathf.Lerp(1f, minOffsetScale, normalizedDistance);
// 실제 적용할 오프셋 계산
Vector3 scaledOffset = offset * offsetScale;
// 플레이어 위치를 스크린 좌표로 변환
Vector3 screenPosition = mainCamera.WorldToScreenPoint(playerTransform.position);
screenPosition.z = 0;
// 위치 적용
transform.position = screenPosition + scaledOffset;
}
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);
}
public void ShowAndHide(string text)
{
// 텍스트 설정
if (bubbleText != null)
bubbleText.text = text;
// 말풍선 활성화
gameObject.SetActive(true);
canvasGroup.alpha = 1f;
// 이전에 실행 중인 코루틴이 있다면 중지
if (hideCoroutine != null)
StopCoroutine(hideCoroutine);
// 3초 후 자동 숨김 코루틴 시작
hideCoroutine = StartCoroutine(HideAfterDelay(3f));
}
// 일정 시간 후 말풍선을 숨기는 코루틴
private IEnumerator HideAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
HideMessage();
hideCoroutine = null;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91cfaa5ec19c50b41ac2d6c542b51cd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -10,8 +10,8 @@ public class InteractionController : MonoBehaviour
{
[SerializeField] LayerMask interactionLayerMask;
[Header("UI 연동")] [SerializeField]
HousingCanvasController housingCanvasController;
[Header("UI 연동")]
[SerializeField] private HousingCanvasController housingCanvasController;
[SerializeField] private InteractionAnimationPanelController interactionAnimationPanelController;
@ -23,6 +23,7 @@ public class InteractionController : MonoBehaviour
// 상호작용 가능한 사물 범위에 들어올 때
private void OnTriggerEnter(Collider other)
{
PlayerStats.Instance.HideBubble();
if (other.gameObject.layer == LayerMask.NameToLayer("NPC"))
{
housingCanvasController.ShowNpcInteractionButton(() =>
@ -48,6 +49,7 @@ public class InteractionController : MonoBehaviour
if (interactionLayerMask == (interactionLayerMask | (1 << other.gameObject.layer)))
{
PlayerStats.Instance.ShowBubble();
housingCanvasController.HideInteractionButton();
housingCanvasController.interactionTextsController.InitInteractionTexts();
}
@ -58,12 +60,18 @@ public class InteractionController : MonoBehaviour
{
HousingConstants.interactions.TryGetValue(interactionType, out var interactionTexts);
housingCanvasController.ShowInteractionButton(interactionTexts.ActionText, interactionTexts.DescriptionText,
() =>
housingCanvasController.ShowInteractionButton(interactionTexts.ActionText,interactionTexts.DescriptionText,()=>
{
if (PlayerStats.Instance.CanPerformByHealth(interactionType))
{
PlayerStats.Instance.PerformAction(interactionType);
if (interactionType == ActionType.Work)
{
if (!PlayerStats.Instance.CanWork()) // 출근 가능한 시간이 아닐 경우
{
PlayerStats.Instance.ShowAndHideBubble("출근 시간이 아냐");
return;
}
}
if (interactionType == ActionType.Dungeon)
{
@ -72,12 +80,14 @@ public class InteractionController : MonoBehaviour
else
{
GameManager.Instance.PlayInteractionSound(interactionType);
interactionAnimationPanelController.ShowAnimationPanel(interactionType,
interactionTexts.AnimationText);
interactionAnimationPanelController.ShowAnimationPanel(interactionType,interactionTexts.AnimationText);
}
PlayerStats.Instance.PerformAction(interactionType);
}
else
{
PlayerStats.Instance.ShowAndHideBubble("체력이 없어...");
housingCanvasController.interactionTextsController.ActiveTexts(interactionTexts.LackOfHealth);
}
});

View File

@ -25,6 +25,8 @@ public class InteractionAnimationPanelController : MonoBehaviour
public void ShowAnimationPanel(ActionType actionType, string animationText)
{
PlayerStats.Instance.HideBubble();
// 1) 패널 활성화
panel.SetActive(true);
// 2) 기존 코루틴 정리
@ -108,6 +110,10 @@ public class InteractionAnimationPanelController : MonoBehaviour
StopCoroutine(_autoHideCoroutine);
_autoHideCoroutine = null;
}
// 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함
PlayerStats.Instance.CheckAbsent();
PlayerStats.Instance.ShowBubble();
}
public void TutorialSleepAnimation()

View File

@ -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": "내가... 해고? 내가?",
"nextId": "fairy_zero_4",
"phase": "zero"
},
{
"id": "fairy_zero_4",
"name": " ",
"text": "그 날 서울시 어느 동네에서, 한 34세의 남성의 절규 소리가 울려퍼졌다.",
"nextId": "",
"phase": "zero"
}
]
}

BIN
Assets/Resources/Prefabs/MessagePanel.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 45ae814acb957c54785f24aefe6843e8
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -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

View File

@ -87,6 +87,10 @@ public class FairyDialogueManager
{
StartPhaseDialogue("end");
}
else if (phase == GamePhase.ZeroEnd)
{
StartPhaseDialogue("zero");
}
}
// 단계별 시작 대화 찾기 및 시작

View File

@ -27,7 +27,11 @@ public partial class GameManager : Singleton<GameManager>
{
// 오디오 초기화
InitializeAudio();
PlayerStats.Instance.OnDayEnded += AdvanceDay;
// 이벤트 할당
PlayerStats.Instance.OnDayEnded += AdvanceDay; // 날짜 변경
PlayerStats.Instance.ZeroReputation += ZeroReputationEnd; // 평판 0 엔딩
//패널 매니저 생성
panelManager = Instantiate(Resources.Load<GameObject>("Prefabs/PanelManager")).GetComponent<PanelManager>();
}

View File

@ -16,10 +16,15 @@ 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;
}
}*/
}
// 던전 스테이지와 평판 수치로 엔딩 판별