DEG-137 [Feat] 출근 수정

This commit is contained in:
Sehyeon 2025-05-12 14:49:57 +09:00
parent 51c2545e35
commit e01987c1d8
13 changed files with 288 additions and 12 deletions

View File

@ -29,7 +29,7 @@ public class PlayerStats : MonoBehaviour
public event Action OnDayEnded; public event Action OnDayEnded;
public event Action Exhaustion; // 탈진 public event Action Exhaustion; // 탈진
public event Action Overslept; // 결근(늦잠) public event Action Overslept; // 늦잠
public event Action ZeroReputation; // 평판 0 이벤트 public event Action ZeroReputation; // 평판 0 이벤트
public event Action<StatsChangeData> OnStatsChanged; // 스탯 변경 이벤트 public event Action<StatsChangeData> OnStatsChanged; // 스탯 변경 이벤트
public event Action OnWorked; // 퇴근 이벤트 (출근 이후 집에 돌아올 시간에 발생) public event Action OnWorked; // 퇴근 이벤트 (출근 이후 집에 돌아올 시간에 발생)
@ -37,6 +37,18 @@ public class PlayerStats : MonoBehaviour
private float previousAddHealth = 0f; private float previousAddHealth = 0f;
public static PlayerStats Instance; 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() private void Awake()
{ {
if (Instance == null) if (Instance == null)
@ -59,8 +71,63 @@ public class PlayerStats : MonoBehaviour
{ {
_valueByAction = new ValueByAction(); _valueByAction = new ValueByAction();
_valueByAction.Initialize(); // 값 초기화 _valueByAction.Initialize(); // 값 초기화
LoadMessagePanel();
CheckBubble();
} }
#region (Bubble)
private void LoadMessagePanel()
{
GameObject messagePanelPrefab = Resources.Load<GameObject>("Prefabs/MessagePanel");
if (messagePanelPrefab != null)
{
Canvas canvas = FindObjectOfType<Canvas>();
messagePanelInstance = Instantiate(messagePanelPrefab, canvas.transform);
speechBubbleFollower = messagePanelInstance.GetComponent<SpeechBubbleFollower>();
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) public bool CanPerformByHealth(ActionType actionType)
{ {
@ -69,6 +136,23 @@ public class PlayerStats : MonoBehaviour
return (HealthStat >= (effect.healthChange * -1)); 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) public void PerformAction(ActionType actionType)
{ {
@ -86,6 +170,7 @@ public class PlayerStats : MonoBehaviour
// 스탯 - 시간이 변경된 이후 퇴근 이벤트 발생 // 스탯 - 시간이 변경된 이후 퇴근 이벤트 발생
if (actionType == ActionType.Work) if (actionType == ActionType.Work)
{ {
_hasWorkedToday = true;
OnWorked?.Invoke(); OnWorked?.Invoke();
} }
} }
@ -156,6 +241,11 @@ public class PlayerStats : MonoBehaviour
// 하루가 실제로 종료된 경우에만 이벤트 발생 // 하루가 실제로 종료된 경우에만 이벤트 발생
if (isDayEnded) if (isDayEnded)
{ {
// 결근 관련 변수 초기화
_hasWorkedToday = false;
_hasCheckedAbsenceToday = false;
hasShownBubbleToday = false;
OnDayEnded?.Invoke(); OnDayEnded?.Invoke();
} }
} }
@ -178,11 +268,13 @@ public class PlayerStats : MonoBehaviour
public void ModifyTime(float time, ActionType actionType) public void ModifyTime(float time, ActionType actionType)
{ {
TimeStat += time; TimeStat += time;
if (TimeStat >= _gameConstants.maxTime) if (TimeStat >= _gameConstants.maxTime)
{ {
EndDay(time, actionType); EndDay(time, actionType);
} }
CheckBubble();
} }
public void ModifyHealth(float health) public void ModifyHealth(float health)
@ -211,11 +303,17 @@ public class PlayerStats : MonoBehaviour
public void ModifyReputation(float reputation) public void ModifyReputation(float reputation)
{ {
// float 연산 시 계산 오차가 발생할 수도 있기에 소수점 두 번째에서 반올림하도록 처리 // float 연산 시 계산 오차가 발생할 수도 있기에 소수점 두 번째에서 반올림하도록 처리
ReputationStat = Mathf.Round((ReputationStat + reputation) * 100f) / 100f; if(ReputationStat > 0)
{
ReputationStat = Mathf.Round((ReputationStat + reputation) * 100f) / 100f;
}
else
{
ReputationStat = 0f;
}
if (ReputationStat <= 0) if (ReputationStat <= 0)
{ {
Debug.Log("당신의 평판은 0입니다..;");
ZeroReputation?.Invoke(); ZeroReputation?.Invoke();
ReputationStat = 0.0f; ReputationStat = 0.0f;
} }

View File

@ -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<RectTransform>();
canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup == null)
canvasGroup = gameObject.AddComponent<CanvasGroup>();
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);
}
}

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

@ -61,6 +61,17 @@ public class InteractionController : MonoBehaviour
{ {
if (PlayerStats.Instance.CanPerformByHealth(interactionType)) 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); PlayerStats.Instance.PerformAction(interactionType);
if (interactionType == ActionType.Dungeon) if (interactionType == ActionType.Dungeon)
@ -96,6 +107,8 @@ public class InteractionController : MonoBehaviour
housingCanvasController.ShowSuddenEventPanel("부장님이 퇴근을 안하셔.. 야근할까?", () => housingCanvasController.ShowSuddenEventPanel("부장님이 퇴근을 안하셔.. 야근할까?", () =>
{ {
//Todo: 컷씬과 스테이터스 변경 //Todo: 컷씬과 스테이터스 변경
// 체력상 가능한지 확인 이후 행동 수행
housingCanvasController.HideSuddenEventPanel(); housingCanvasController.HideSuddenEventPanel();
}); });
break; break;

View File

@ -107,6 +107,9 @@ public class InteractionAnimationPanelController : MonoBehaviour
StopCoroutine(_autoHideCoroutine); StopCoroutine(_autoHideCoroutine);
_autoHideCoroutine = null; _autoHideCoroutine = null;
} }
// 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함
PlayerStats.Instance.CheckAbsent();
} }
public void TutorialSleepAnimation() public void TutorialSleepAnimation()

View File

@ -146,6 +146,55 @@
"text": "... GameManager.Instance.ShowCredit(GamePhase.End);", "text": "... GameManager.Instance.ShowCredit(GamePhase.End);",
"nextId": "", "nextId": "",
"phase": "end" "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"
} }
] ]
} }

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, // 인트로 설명문 Intro, // 인트로 설명문
Gameplay, // 게임 진행 팁? 등 Gameplay, // 게임 진행 팁? 등
End // 엔딩 대화 End, // 엔딩 대화
ZeroEnd
} }
public class ChatWindowController : MonoBehaviour, IPointerClickHandler public class ChatWindowController : MonoBehaviour, IPointerClickHandler
@ -90,6 +91,7 @@ public class ChatWindowController : MonoBehaviour, IPointerClickHandler
if (_inputQueue.Count > 0) if (_inputQueue.Count > 0)
{ {
ShowNextDialogue(); ShowNextDialogue();
PlayerStats.Instance.HideBubble();
} }
} }
@ -111,6 +113,8 @@ public class ChatWindowController : MonoBehaviour, IPointerClickHandler
StopCoroutine(_clickCoroutine); StopCoroutine(_clickCoroutine);
_clickCoroutine = null; _clickCoroutine = null;
} }
PlayerStats.Instance.ShowBubble();
} }
#endregion #endregion

View File

@ -86,6 +86,10 @@ public class FairyDialogueManager
else if (phase == GamePhase.End) else if (phase == GamePhase.End)
{ {
StartPhaseDialogue("end"); StartPhaseDialogue("end");
}
else if (phase == GamePhase.ZeroEnd)
{
StartPhaseDialogue("zero");
} }
} }

View File

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

View File

@ -16,9 +16,14 @@ public partial class GameManager
public void ClearStage() public void ClearStage()
{ {
Debug.Log($"스테이지 레벨 {stageLevel}을 클리어 하셨습니다!");
stageLevel++; stageLevel++;
} }
private void ZeroReputationEnd()
{
// npc와의 대화 출력, Phase = zero
StartNPCDialogue(GamePhase.ZeroEnd);
}
// 엔딩 관련 메서드. 7일차에 실행 // 엔딩 관련 메서드. 7일차에 실행
private void TriggerTimeEnding() private void TriggerTimeEnding()
@ -27,10 +32,10 @@ public partial class GameManager
StartNPCDialogue(GamePhase.End); StartNPCDialogue(GamePhase.End);
// 플레이어 상태에 따라 엔딩 판별 // 플레이어 상태에 따라 엔딩 판별
EndingType endingType = DetermineEnding(); // EndingType endingType = DetermineEnding();
// 엔딩 타입에 따라 다른 씬이나 UI 표시 // 엔딩 타입에 따라 다른 씬이나 UI 표시
switch (endingType) /*switch (endingType)
{ {
case EndingType.Normal: case EndingType.Normal:
Debug.Log("던전 공략 성공"); Debug.Log("던전 공략 성공");
@ -44,7 +49,7 @@ public partial class GameManager
Debug.Log("던전 공략 성공과 훌륭한 평판 작"); Debug.Log("던전 공략 성공과 훌륭한 평판 작");
break; break;
} }*/
} }
// 던전 스테이지와 평판 수치로 엔딩 판별 // 던전 스테이지와 평판 수치로 엔딩 판별