Add duty free systems and travel stage updates

This commit is contained in:
jongjae0305
2026-07-09 02:50:48 +09:00
parent 445d74d462
commit 750e9ce0a6
340 changed files with 21825 additions and 1197 deletions
+357 -12
View File
@@ -1,15 +1,23 @@
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
// 왼쪽 끝으로 이동한 배경을 오른쪽 끝으로 재배치하는 스크립트
[ExecuteAlways]
public class BackgroundLoop : MonoBehaviour {
public Sprite[] backgroundSprites; // 순서대로 교체할 공항 배경 스프라이트
public bool useResourceBackground = true; // Resources 폴더의 맵 배경을 자동으로 사용
public bool fitToMainCamera = true; // 맵 배경을 카메라 화면 크기에 맞춰 표시
public float cameraFitPadding = 1f; // 가장자리 빈틈 방지용 배경 확대 여유
public string resourceBackgroundPath = ""; // 맵 데이터가 없을 때 사용할 Resources 배경
public int startSpriteIndex = 0; // 이 배경 오브젝트가 시작할 스프라이트 번호
public int spriteAdvanceOnLoop = 2; // 배경 오브젝트가 두 장이므로 재배치될 때 두 칸씩 진행
private static string activeResourceBackgroundPath = "";
private static string[] activeResourceBackgroundPaths = new string[0];
private static int activeBackgroundLoopCount = 1;
private static float activeCameraFitScaleMultiplier = 1f;
private static float activeVerticalOffset = 0f;
private float width; // 배경의 가로 길이
private int currentSpriteIndex; // 현재 표시 중인 스프라이트 번호
@@ -17,9 +25,24 @@ public class BackgroundLoop : MonoBehaviour {
private BoxCollider2D backgroundCollider; // 루프 폭 계산용 콜라이더
private Sprite[] originalBackgroundSprites; // 맵 배경 리소스가 없을 때 되돌릴 기본 배경
private bool originalBackgroundSpritesCached = false;
private Vector3 originalLocalPosition; // 배경 루프를 초기 배치로 되돌릴 때 사용할 위치
private Vector3 originalLocalScale = Vector3.one; // 자동 맞춤 해제 시 되돌릴 기본 스케일
private bool originalTransformCached = false;
public static void ApplyResourceBackground(string resourcePath) {
activeResourceBackgroundPath = resourcePath;
ApplyResourceBackground(string.IsNullOrEmpty(resourcePath)
? new string[0]
: new string[] { resourcePath });
}
public static void ApplyResourceBackground(string[] resourcePaths) {
ApplyResourceBackground(resourcePaths, 1f, 0f);
}
public static void ApplyResourceBackground(string[] resourcePaths, float cameraFitScaleMultiplier, float verticalOffset) {
activeResourceBackgroundPaths = NormalizeResourcePaths(resourcePaths);
activeCameraFitScaleMultiplier = Mathf.Max(0.1f, cameraFitScaleMultiplier);
activeVerticalOffset = verticalOffset;
BackgroundLoop[] backgroundLoops = FindObjectsByType<BackgroundLoop>(FindObjectsSortMode.None);
for(int i = 0; i < backgroundLoops.Length; i++)
@@ -29,6 +52,9 @@ public class BackgroundLoop : MonoBehaviour {
backgroundLoops[i].InitializeBackground();
}
}
AlignLoopPositions(backgroundLoops);
AssignSequentialSpriteIndices(backgroundLoops);
}
private void Awake() {
@@ -59,10 +85,12 @@ public class BackgroundLoop : MonoBehaviour {
private void InitializeBackground() {
CacheComponents();
CacheOriginalBackgroundSprites();
CacheOriginalTransform();
EnsureResourceBackground();
currentSpriteIndex = NormalizeSpriteIndex(startSpriteIndex);
ApplyBackgroundSprite(currentSpriteIndex);
FitSpriteToCamera();
SyncColliderSize();
RefreshWidth();
}
@@ -80,33 +108,92 @@ public class BackgroundLoop : MonoBehaviour {
}
private void EnsureResourceBackground() {
if(!useResourceBackground)
bool hasActiveResourceBackground = activeResourceBackgroundPaths != null
&& activeResourceBackgroundPaths.Length > 0;
if(!useResourceBackground && !hasActiveResourceBackground)
{
return;
}
string finalResourcePath = !string.IsNullOrEmpty(activeResourceBackgroundPath)
? activeResourceBackgroundPath
: resourceBackgroundPath;
string[] finalResourcePaths = hasActiveResourceBackground
? activeResourceBackgroundPaths
: NormalizeResourcePaths(string.IsNullOrEmpty(resourceBackgroundPath)
? new string[0]
: new string[] { resourceBackgroundPath });
if(string.IsNullOrEmpty(finalResourcePath))
if(finalResourcePaths.Length == 0)
{
backgroundSprites = originalBackgroundSprites;
return;
}
Sprite resourceSprite = Resources.Load<Sprite>(finalResourcePath);
if(resourceSprite == null)
Sprite[] resourceSprites = LoadResourceBackgroundSprites(finalResourcePaths);
if(resourceSprites.Length == 0)
{
return;
}
if(backgroundSprites != null && backgroundSprites.Length == 1 && backgroundSprites[0] == resourceSprite)
if(SpriteArraysMatch(backgroundSprites, resourceSprites))
{
return;
}
backgroundSprites = new Sprite[] { resourceSprite };
backgroundSprites = resourceSprites;
}
private Sprite[] LoadResourceBackgroundSprites(string[] resourcePaths) {
Sprite[] loadedSprites = new Sprite[resourcePaths.Length];
int loadedCount = 0;
for(int i = 0; i < resourcePaths.Length; i++)
{
Sprite resourceSprite = LoadResourceBackgroundSprite(resourcePaths[i]);
if(resourceSprite == null)
{
continue;
}
loadedSprites[loadedCount] = resourceSprite;
loadedCount++;
}
if(loadedCount == loadedSprites.Length)
{
return loadedSprites;
}
Sprite[] compactSprites = new Sprite[loadedCount];
for(int i = 0; i < loadedCount; i++)
{
compactSprites[i] = loadedSprites[i];
}
return compactSprites;
}
private Sprite LoadResourceBackgroundSprite(string resourcePath) {
Sprite resourceSprite = Resources.Load<Sprite>(resourcePath);
if(resourceSprite != null)
{
return resourceSprite;
}
#if UNITY_EDITOR
string assetPath = "Assets/Resources/" + resourcePath + ".png";
Sprite editorSprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if(editorSprite != null)
{
return editorSprite;
}
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture != null)
{
return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 8f);
}
#endif
return null;
}
private void CacheOriginalBackgroundSprites() {
@@ -119,6 +206,17 @@ public class BackgroundLoop : MonoBehaviour {
originalBackgroundSpritesCached = true;
}
private void CacheOriginalTransform() {
if(originalTransformCached)
{
return;
}
originalLocalPosition = transform.localPosition;
originalLocalScale = transform.localScale;
originalTransformCached = true;
}
private void RefreshWidth() {
if(spriteRenderer != null && spriteRenderer.sprite != null)
{
@@ -147,13 +245,77 @@ public class BackgroundLoop : MonoBehaviour {
backgroundCollider.isTrigger = true;
}
private void FitSpriteToCamera() {
if(spriteRenderer == null || spriteRenderer.sprite == null)
{
return;
}
if(!fitToMainCamera)
{
transform.localScale = originalLocalScale;
ApplyVerticalOffset();
return;
}
Camera targetCamera = Camera.main;
if(targetCamera == null || !targetCamera.orthographic)
{
ApplyVerticalOffset();
return;
}
Vector2 spriteSize = spriteRenderer.sprite.bounds.size;
if(spriteSize.x <= 0f || spriteSize.y <= 0f)
{
ApplyVerticalOffset();
return;
}
float cameraHeight = targetCamera.orthographicSize * 2f;
float cameraWidth = cameraHeight * GetCameraAspect(targetCamera);
float parentScaleX = transform.parent != null
? Mathf.Abs(transform.parent.lossyScale.x)
: 1f;
float parentScaleY = transform.parent != null
? Mathf.Abs(transform.parent.lossyScale.y)
: 1f;
float scaleX = cameraWidth / (spriteSize.x * Mathf.Max(parentScaleX, 0.0001f));
float scaleY = cameraHeight / (spriteSize.y * Mathf.Max(parentScaleY, 0.0001f));
float fitScale = Mathf.Max(scaleX, scaleY)
* Mathf.Max(cameraFitPadding, 1f)
* activeCameraFitScaleMultiplier;
float signX = Mathf.Sign(originalLocalScale.x);
float signY = Mathf.Sign(originalLocalScale.y);
if(Mathf.Approximately(signX, 0f))
{
signX = 1f;
}
if(Mathf.Approximately(signY, 0f))
{
signY = 1f;
}
transform.localScale = new Vector3(fitScale * signX, fitScale * signY, originalLocalScale.z);
ApplyVerticalOffset();
}
private void ApplyVerticalOffset() {
Vector3 localPosition = transform.localPosition;
localPosition.y = originalLocalPosition.y + activeVerticalOffset;
transform.localPosition = localPosition;
}
// 위치를 리셋하는 메서드
private void Reposition() {
Vector2 offset = new Vector2(width * 2f, 0);
transform.position = (Vector2) transform.position + offset;
currentSpriteIndex = NormalizeSpriteIndex(currentSpriteIndex + spriteAdvanceOnLoop);
currentSpriteIndex = NormalizeSpriteIndex(currentSpriteIndex + GetSpriteAdvanceOnLoop());
ApplyBackgroundSprite(currentSpriteIndex);
FitSpriteToCamera();
SyncColliderSize();
RefreshWidth();
}
@@ -189,4 +351,187 @@ public class BackgroundLoop : MonoBehaviour {
spriteRenderer.color = Color.white;
}
}
private void SetCurrentSpriteIndex(int index) {
currentSpriteIndex = NormalizeSpriteIndex(index);
ApplyBackgroundSprite(currentSpriteIndex);
FitSpriteToCamera();
SyncColliderSize();
RefreshWidth();
}
private float GetCurrentWidth() {
if(width > 0f)
{
return width;
}
RefreshWidth();
return width;
}
private int GetSpriteAdvanceOnLoop() {
if(backgroundSprites != null && backgroundSprites.Length > 1 && activeBackgroundLoopCount > 1)
{
return activeBackgroundLoopCount;
}
return Mathf.Max(1, spriteAdvanceOnLoop);
}
private static float GetCameraAspect(Camera targetCamera) {
if(targetCamera.aspect > 0f)
{
return targetCamera.aspect;
}
if(Screen.height > 0)
{
return (float) Screen.width / Screen.height;
}
return 16f / 9f;
}
private static string[] NormalizeResourcePaths(string[] resourcePaths) {
if(resourcePaths == null || resourcePaths.Length == 0)
{
return new string[0];
}
string[] normalizedPaths = new string[resourcePaths.Length];
int pathCount = 0;
for(int i = 0; i < resourcePaths.Length; i++)
{
if(string.IsNullOrEmpty(resourcePaths[i]))
{
continue;
}
normalizedPaths[pathCount] = resourcePaths[i];
pathCount++;
}
if(pathCount == normalizedPaths.Length)
{
return normalizedPaths;
}
string[] compactPaths = new string[pathCount];
for(int i = 0; i < pathCount; i++)
{
compactPaths[i] = normalizedPaths[i];
}
return compactPaths;
}
private static bool SpriteArraysMatch(Sprite[] firstSprites, Sprite[] secondSprites) {
if(firstSprites == null || secondSprites == null || firstSprites.Length != secondSprites.Length)
{
return false;
}
for(int i = 0; i < firstSprites.Length; i++)
{
if(firstSprites[i] != secondSprites[i])
{
return false;
}
}
return true;
}
private static void AlignLoopPositions(BackgroundLoop[] backgroundLoops) {
if(backgroundLoops == null || backgroundLoops.Length <= 1)
{
activeBackgroundLoopCount = 1;
return;
}
int validCount = 0;
for(int i = 0; i < backgroundLoops.Length; i++)
{
if(backgroundLoops[i] != null)
{
validCount++;
}
}
if(validCount <= 1)
{
activeBackgroundLoopCount = Mathf.Max(1, validCount);
return;
}
activeBackgroundLoopCount = validCount;
BackgroundLoop[] sortedLoops = new BackgroundLoop[validCount];
int sortedIndex = 0;
for(int i = 0; i < backgroundLoops.Length; i++)
{
if(backgroundLoops[i] != null)
{
sortedLoops[sortedIndex] = backgroundLoops[i];
sortedIndex++;
}
}
System.Array.Sort(sortedLoops, (first, second) =>
first.originalLocalPosition.x.CompareTo(second.originalLocalPosition.x));
float loopWidth = sortedLoops[0].GetCurrentWidth();
if(loopWidth <= 0f)
{
return;
}
float baseX = sortedLoops[0].originalLocalPosition.x;
for(int i = 0; i < sortedLoops.Length; i++)
{
Vector3 localPosition = sortedLoops[i].originalLocalPosition;
localPosition.x = baseX + loopWidth * i;
sortedLoops[i].transform.localPosition = localPosition;
}
}
private static void AssignSequentialSpriteIndices(BackgroundLoop[] backgroundLoops) {
if(backgroundLoops == null || backgroundLoops.Length == 0)
{
return;
}
int validCount = 0;
for(int i = 0; i < backgroundLoops.Length; i++)
{
if(backgroundLoops[i] != null)
{
validCount++;
}
}
if(validCount == 0)
{
return;
}
BackgroundLoop[] sortedLoops = new BackgroundLoop[validCount];
int sortedIndex = 0;
for(int i = 0; i < backgroundLoops.Length; i++)
{
if(backgroundLoops[i] != null)
{
sortedLoops[sortedIndex] = backgroundLoops[i];
sortedIndex++;
}
}
System.Array.Sort(sortedLoops, (first, second) =>
first.originalLocalPosition.x.CompareTo(second.originalLocalPosition.x));
for(int i = 0; i < sortedLoops.Length; i++)
{
sortedLoops[i].SetCurrentSpriteIndex(i);
}
}
}
+22
View File
@@ -0,0 +1,22 @@
using System;
[Serializable]
public class CourseProfile {
public CourseStep[] openingSteps;
public CourseStep[] loopSteps;
public CourseStep[] GetOpeningSteps() {
return CourseStepFactory.CloneSteps(openingSteps);
}
public CourseStep[] GetLoopSteps() {
return CourseStepFactory.CloneSteps(loopSteps);
}
public static CourseProfile Create(CourseStep[] openingSteps, CourseStep[] loopSteps) {
return new CourseProfile {
openingSteps = CourseStepFactory.CloneSteps(openingSteps),
loopSteps = CourseStepFactory.CloneSteps(loopSteps)
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b89f600e13ac4d13a6ed5691d11e8e0d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+1 -3
View File
@@ -6,7 +6,5 @@ public enum CourseStepKind {
ForceHit,
HealthItem,
InvincibleItem,
ShieldItem,
SpeedItem,
MileageCardItem
EnergyDrinkItem
}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 29d088f4951c41a7a41a6c85a5fd94ff
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using static CourseStepFactory;
public static class AmericaCourseProfile {
public static CourseProfile Create() {
return CourseProfile.Create(
new CourseStep[] {
Basic(GroundY, 0.60f, Platform.MileagePattern.SafeLine),
Jump(Platform.PlatformPattern.LowMid, 0.58f),
Slide(SlideObstaclePattern.SlideLayout.WidePair, 0.58f),
Basic(GroundY, 0.56f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.EnergyDrink),
Aerial(AerialY + 0.20f, 1.35f),
Basic(GroundY, 0.58f, Platform.MileagePattern.SafeLine)
},
new CourseStep[] {
Basic(GroundY, 0.56f, Platform.MileagePattern.SafeLine),
Jump(Platform.PlatformPattern.LowLeft, 0.54f),
Slide(SlideObstaclePattern.SlideLayout.RightPair, 0.52f),
Basic(GroundY, 0.50f, Platform.MileagePattern.JumpArc),
Jump(Platform.PlatformPattern.LowRight, 0.52f),
Basic(GroundY, 0.50f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.HealthSushi),
Aerial(AerialY + 0.22f, 1.24f),
Slide(SlideObstaclePattern.SlideLayout.WidePair, 0.50f),
Basic(GroundY, 0.48f, Platform.MileagePattern.SlideLine),
Jump(Platform.PlatformPattern.LowMid, 0.50f),
Slide(SlideObstaclePattern.SlideLayout.LeftPair, 0.48f),
Basic(GroundY, 0.50f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.EnergyDrink),
Jump(Platform.PlatformPattern.LowLeft, 0.50f),
Jump(Platform.PlatformPattern.LowRight, 0.50f),
Basic(GroundY, 0.52f, Platform.MileagePattern.SafeLine),
Aerial(AerialY + 0.12f, 1.18f),
Basic(GroundY, 0.50f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.InvincibleRamen),
Slide(SlideObstaclePattern.SlideLayout.CenterPair, 0.48f),
Jump(Platform.PlatformPattern.LowMid, 0.50f),
Basic(GroundY, 0.58f, Platform.MileagePattern.SafeLine)
});
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3473696a9ce54b0cbecb0b7c3aa8aebd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using static CourseStepFactory;
public static class ChinaCourseProfile {
public static CourseProfile Create() {
return CourseProfile.Create(
new CourseStep[] {
Basic(GroundY, 0.66f, Platform.MileagePattern.SafeLine),
Jump(Platform.PlatformPattern.LowRight, 0.68f),
Basic(GroundY, 0.62f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.HealthSushi),
Slide(SlideObstaclePattern.SlideLayout.LeftPair, 0.66f),
Basic(GroundY, 0.62f, Platform.MileagePattern.SafeLine),
Aerial(AerialY + 0.12f, 1.72f),
Basic(GroundY, 0.66f, Platform.MileagePattern.SafeLine)
},
new CourseStep[] {
Basic(GroundY, 0.62f, Platform.MileagePattern.SafeLine),
Jump(Platform.PlatformPattern.LowLeft, 0.64f),
Basic(GroundY, 0.58f, Platform.MileagePattern.JumpArc),
Slide(SlideObstaclePattern.SlideLayout.RightPair, 0.62f),
Basic(GroundY, 0.60f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.EnergyDrink),
Jump(Platform.PlatformPattern.LowRight, 0.60f),
Basic(GroundY, 0.58f, Platform.MileagePattern.SafeLine),
Aerial(AerialY + 0.14f, 1.55f),
Basic(GroundY, 0.62f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.InvincibleRamen),
Slide(SlideObstaclePattern.SlideLayout.LeftPair, 0.60f),
Basic(GroundY, 0.58f, Platform.MileagePattern.SlideLine),
Jump(Platform.PlatformPattern.LowMid, 0.60f),
Slide(SlideObstaclePattern.SlideLayout.CenterPair, 0.58f),
Basic(GroundY, 0.64f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.HealthSushi),
Aerial(AerialY + 0.05f, 1.70f),
Basic(GroundY, 0.66f, Platform.MileagePattern.SafeLine)
});
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 513d2fc410734352ab7a51ff2f8b5d9f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,55 @@
using static CourseStepFactory;
public static class FallbackCourseProfile {
public static CourseProfile CreateTutorialFallback() {
return CourseProfile.Create(
new CourseStep[] {
Jump(GameManager.TutorialGuideType.Jump, "점프", "가방 장애물이 보이면 왼쪽 클릭으로 뛰어넘습니다."),
Basic(GroundY, LandingSpacing),
Aerial(GameManager.TutorialGuideType.DoubleJump, "2단 점프", "다음 발판이 멀리 있으면 공중에서 한 번 더 왼쪽 클릭합니다."),
Basic(GroundY, LandingSpacing),
Slide(GameManager.TutorialGuideType.Slide, "슬라이드", "천장 표지판이 보이면 오른쪽 클릭을 누르고 지나갑니다."),
Basic(GroundY, LandingSpacing),
ForceHit(GameManager.TutorialGuideType.Damage, "피격", "이번 구간은 일부러 맞아 목숨과 속도 감소를 확인합니다."),
Basic(GroundY, LandingSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, GameplayItemCatalog.GetDisplayName(GameManager.TutorialItemType.HealthSushi, Platform.PlatformSkin.Airport), "김밥을 먹으면 잃은 목숨을 1 회복합니다.", Platform.ItemPattern.HealthSushi),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, GameplayItemCatalog.GetDisplayName(GameManager.TutorialItemType.InvincibleRamen, Platform.PlatformSkin.Airport), "바나나우유를 먹고 다음 충돌을 피해 없이 지나가 봅니다.", Platform.ItemPattern.InvincibleRamen),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Basic(GroundY, BasicSpacing),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, GameplayItemCatalog.GetDisplayName(GameManager.TutorialItemType.EnergyDrink, Platform.PlatformSkin.Airport), "에너지 드링크는 속도를 조금 회복해 일정이 늦어지는 것을 줄입니다.", Platform.ItemPattern.EnergyDrink),
Basic(GroundY, BasicSpacing),
Basic(GroundY, BasicSpacing)
},
CreateGeneralFallbackLoop());
}
public static CourseProfile CreateGeneralFallback() {
return CourseProfile.Create(
new CourseStep[] {
Basic(),
Basic()
},
CreateGeneralFallbackLoop());
}
private static CourseStep[] CreateGeneralFallbackLoop() {
return new CourseStep[] {
Basic(),
Jump(),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.WidePair),
Basic(),
Jump(Platform.PlatformPattern.LowLeft),
Slide(SlideObstaclePattern.SlideLayout.LeftPair),
Basic(),
Jump(Platform.PlatformPattern.LowRight),
Aerial(AerialY, AerialSpacing),
Slide(SlideObstaclePattern.SlideLayout.RightPair),
Jump(Platform.PlatformPattern.LowMid),
Basic(),
Slide(SlideObstaclePattern.SlideLayout.CenterPair)
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c10f2b5787bb465ab16291a98eb7a51d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using static CourseStepFactory;
public static class JapanCourseProfile {
public static CourseProfile Create() {
return CourseProfile.Create(
new CourseStep[] {
Basic(GroundY, 0.72f, Platform.MileagePattern.SafeLine),
Basic(GroundY, 0.70f, Platform.MileagePattern.SafeLine),
Jump(Platform.PlatformPattern.LowMid, 0.78f),
Basic(GroundY, 0.70f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.HealthSushi),
Basic(GroundY, 0.68f, Platform.MileagePattern.SafeLine),
Slide(SlideObstaclePattern.SlideLayout.CenterPair, 0.76f),
Basic(GroundY, 0.70f, Platform.MileagePattern.SafeLine)
},
new CourseStep[] {
Basic(GroundY, 0.68f, Platform.MileagePattern.SafeLine),
Jump(Platform.PlatformPattern.LowMid, 0.74f),
Basic(GroundY, 0.70f, Platform.MileagePattern.JumpArc),
Basic(GroundY, 0.66f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.HealthSushi),
Slide(SlideObstaclePattern.SlideLayout.CenterPair, 0.74f),
Basic(GroundY, 0.70f, Platform.MileagePattern.SlideLine),
Aerial(AerialY, 2.05f),
Basic(GroundY, 0.72f, Platform.MileagePattern.SafeLine),
Jump(Platform.PlatformPattern.LowLeft, 0.72f),
Basic(GroundY, 0.66f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.InvincibleRamen),
Basic(GroundY, 0.68f, Platform.MileagePattern.SafeLine),
Slide(SlideObstaclePattern.SlideLayout.RightPair, 0.72f),
Basic(GroundY, 0.66f, Platform.MileagePattern.None, GameManager.TutorialGuideType.None, "", "", Platform.ItemPattern.EnergyDrink),
Jump(Platform.PlatformPattern.LowRight, 0.72f),
Basic(GroundY, 0.72f, Platform.MileagePattern.SafeLine),
Basic(GroundY, 0.76f, Platform.MileagePattern.SafeLine)
});
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0ec45d923be242e3bf69387973545026
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using static CourseStepFactory;
public static class TutorialCourseProfile {
public static CourseProfile Create() {
return CourseProfile.Create(
new CourseStep[] {
Basic(),
Basic(),
Basic(GroundY, BasicSpacing),
Jump(GameManager.TutorialGuideType.Jump, "점프 구간", "가방 장애물이 오면 왼쪽 클릭으로 뛰어넘습니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Aerial(GameManager.TutorialGuideType.DoubleJump, "2단 점프 구간", "다음 발판이 멀리 떨어져 있습니다.\n공중에서 왼쪽 클릭을 한 번 더 누릅니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Slide(GameManager.TutorialGuideType.Slide, "슬라이드 구간", "천장 장애물이 오면 오른쪽 클릭을 누르고 지나갑니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "김밥", "잃은 목숨을 1 회복합니다.\n방금 줄어든 목숨을 채워 봅니다.", Platform.ItemPattern.HealthSushi),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "바나나우유", "잠깐 무적이 됩니다.\n먹은 뒤 다음 장애물에 일부러 부딪혀 봅니다.", Platform.ItemPattern.InvincibleRamen),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing),
Basic(GroundY, LandingSpacing),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "에너지 드링크", "느려진 속도를 조금 회복합니다.\n방금 떨어진 속도를 되돌려 봅니다.", Platform.ItemPattern.EnergyDrink),
Basic(GroundY, LandingSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "실전 연습", "시간이 지나면 달리기 속도는 조금씩 빨라지고, 장애물에 맞으면 느려집니다.\n방금 먹은 에너지 드링크로 회복한 뒤 출국 게이트까지 달려보세요."),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.SafeLine),
Basic(),
Basic(),
Basic(),
Basic(GroundY, LandingSpacing)
},
CreateEasyLoop());
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 755fb689f3ff4855a3193f4711b10fa5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
+21 -4
View File
@@ -1,15 +1,27 @@
using System;
public enum DutyFreeItemType {
None = 0,
Lunchbox = 1,
MileageCard = 4,
Supplement = 5
}
[Serializable]
public class GameSaveData {
public int currentMapIndex;
public int unlockedMapIndex;
public int totalMileage;
public int bagSlots;
public int shieldStock;
public int lunchboxStock;
public int speedShoesStock;
public int mileageCardStock;
public int supplementPurchases;
public int dutyFreeLimitStageIndex;
public bool purchasedMileageCardThisStage;
public bool purchasedSupplementThisStage;
public DutyFreeItemType equippedSlot1;
public DutyFreeItemType equippedSlot2;
public DutyFreeItemType equippedSlot3;
public int bestScore;
public static GameSaveData CreateDefault() {
@@ -18,10 +30,15 @@ public class GameSaveData {
unlockedMapIndex = 0,
totalMileage = 0,
bagSlots = 1,
shieldStock = 0,
lunchboxStock = 0,
speedShoesStock = 0,
mileageCardStock = 0,
supplementPurchases = 0,
dutyFreeLimitStageIndex = -1,
purchasedMileageCardThisStage = false,
purchasedSupplementThisStage = false,
equippedSlot1 = DutyFreeItemType.None,
equippedSlot2 = DutyFreeItemType.None,
equippedSlot3 = DutyFreeItemType.None,
bestScore = 0
};
}
+140 -2
View File
@@ -66,10 +66,148 @@ public static class GameSaveManager {
saveData.currentMapIndex = Mathf.Min(saveData.currentMapIndex, saveData.unlockedMapIndex);
saveData.totalMileage = Mathf.Max(0, saveData.totalMileage);
saveData.bagSlots = Mathf.Clamp(saveData.bagSlots, 1, 3);
saveData.shieldStock = Mathf.Max(0, saveData.shieldStock);
saveData.lunchboxStock = Mathf.Max(0, saveData.lunchboxStock);
saveData.speedShoesStock = Mathf.Max(0, saveData.speedShoesStock);
saveData.mileageCardStock = Mathf.Max(0, saveData.mileageCardStock);
saveData.supplementPurchases = Mathf.Clamp(saveData.supplementPurchases, 0, 4);
saveData.dutyFreeLimitStageIndex = saveData.dutyFreeLimitStageIndex < 0
? -1
: Mathf.Clamp(saveData.dutyFreeLimitStageIndex, 0, MapDatabase.LastMapIndex);
NormalizeEquippedItems(saveData);
NormalizeDutyFreeStockCapacity(saveData);
NormalizeEquippedItems(saveData);
saveData.bestScore = Mathf.Max(0, saveData.bestScore);
}
private static void NormalizeDutyFreeStockCapacity(GameSaveData saveData) {
int bagSlots = Mathf.Clamp(saveData.bagSlots, 1, 3);
int equippedLunchboxes = CountEquippedItem(saveData, DutyFreeItemType.Lunchbox, bagSlots);
int equippedMileageCards = CountEquippedItem(saveData, DutyFreeItemType.MileageCard, bagSlots);
saveData.lunchboxStock = Mathf.Max(equippedLunchboxes, Mathf.Min(saveData.lunchboxStock, bagSlots));
saveData.mileageCardStock = Mathf.Max(equippedMileageCards, Mathf.Min(saveData.mileageCardStock, bagSlots));
while(saveData.lunchboxStock + saveData.mileageCardStock > bagSlots)
{
if(saveData.mileageCardStock > equippedMileageCards)
{
saveData.mileageCardStock--;
}
else if(saveData.lunchboxStock > equippedLunchboxes)
{
saveData.lunchboxStock--;
}
else
{
break;
}
}
}
private static int CountEquippedItem(GameSaveData saveData, DutyFreeItemType itemType, int bagSlots) {
int count = 0;
for(int i = 0; i < bagSlots; i++)
{
if(GetEquippedSlot(saveData, i) == itemType)
{
count++;
}
}
return count;
}
private static void NormalizeEquippedItems(GameSaveData saveData) {
int bagSlots = Mathf.Clamp(saveData.bagSlots, 1, 3);
if(bagSlots < 3)
{
saveData.equippedSlot3 = DutyFreeItemType.None;
}
if(bagSlots < 2)
{
saveData.equippedSlot2 = DutyFreeItemType.None;
}
saveData.equippedSlot1 = NormalizeItemType(saveData.equippedSlot1);
saveData.equippedSlot2 = NormalizeItemType(saveData.equippedSlot2);
saveData.equippedSlot3 = NormalizeItemType(saveData.equippedSlot3);
int lunchboxCount = 0;
int mileageCardCount = 0;
ClampEquippedSlot(saveData, 0, ref lunchboxCount, ref mileageCardCount);
ClampEquippedSlot(saveData, 1, ref lunchboxCount, ref mileageCardCount);
ClampEquippedSlot(saveData, 2, ref lunchboxCount, ref mileageCardCount);
}
private static DutyFreeItemType NormalizeItemType(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
case DutyFreeItemType.MileageCard:
return itemType;
default:
return DutyFreeItemType.None;
}
}
private static void ClampEquippedSlot(
GameSaveData saveData,
int slotIndex,
ref int lunchboxCount,
ref int mileageCardCount) {
DutyFreeItemType itemType = GetEquippedSlot(saveData, slotIndex);
if(itemType == DutyFreeItemType.None)
{
return;
}
bool overStock = false;
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
overStock = lunchboxCount >= saveData.lunchboxStock;
lunchboxCount++;
break;
case DutyFreeItemType.MileageCard:
overStock = mileageCardCount >= saveData.mileageCardStock;
mileageCardCount++;
break;
}
if(overStock)
{
SetEquippedSlot(saveData, slotIndex, DutyFreeItemType.None);
}
}
private static DutyFreeItemType GetEquippedSlot(GameSaveData saveData, int slotIndex) {
switch(slotIndex)
{
case 0:
return saveData.equippedSlot1;
case 1:
return saveData.equippedSlot2;
case 2:
return saveData.equippedSlot3;
default:
return DutyFreeItemType.None;
}
}
private static void SetEquippedSlot(GameSaveData saveData, int slotIndex, DutyFreeItemType itemType) {
switch(slotIndex)
{
case 0:
saveData.equippedSlot1 = itemType;
break;
case 1:
saveData.equippedSlot2 = itemType;
break;
case 2:
saveData.equippedSlot3 = itemType;
break;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 55433eabfe544a199c2b2899e3256b3b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,178 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public partial class GameFlowManager {
#if UNITY_EDITOR || DEVELOPMENT_BUILD
private void ShowDebugMenu() {
EnsureDebugPreviewData();
gameplayActive = false;
pendingStartGameplay = false;
currentScreen = FlowScreen.DebugMenu;
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
SetDefaultOverlayBackground();
AddTitle("디버그 메뉴");
AddBody("기본 화면과 실전맵을 바로 확인합니다.\n단축키: 1~0, F1/F2");
AddDebugButton("1. 인트로", ShowDebugIntroPreview, new Vector2(-235f, -32f));
AddDebugButton("2. 현재 맵 시작", ShowDebugGameStartPreview, new Vector2(-235f, -74f));
AddDebugButton("3. 현재 출국 실패", delegate { ShowDebugResultPreview(false); }, new Vector2(-235f, -116f));
AddDebugButton("4. 현재 출국 성공", delegate { ShowDebugResultPreview(true); }, new Vector2(-235f, -158f));
AddDebugButton("5. 면세점", ShowDebugDutyFreeShopPreview, new Vector2(-235f, -200f));
AddDebugButton("6. 지도", ShowDebugMapPreview, new Vector2(-235f, -242f));
AddDebugButton("7. 일본 게임시작", delegate { ShowDebugMapGameStartPreview(MapId.Japan); }, new Vector2(235f, -32f));
AddDebugButton("8. 일본 출국성공", delegate { ShowDebugMapResultPreview(MapId.Japan, true); }, new Vector2(235f, -74f));
AddDebugButton("9. 중국 게임시작", delegate { ShowDebugMapGameStartPreview(MapId.China); }, new Vector2(235f, -116f));
AddDebugButton("0. 중국 출국성공", delegate { ShowDebugMapResultPreview(MapId.China, true); }, new Vector2(235f, -158f));
AddDebugButton("F1. 미국 게임시작", delegate { ShowDebugMapGameStartPreview(MapId.America); }, new Vector2(235f, -200f));
AddDebugButton("F2. 미국 출국성공", delegate { ShowDebugMapResultPreview(MapId.America, true); }, new Vector2(235f, -242f));
}
private void AddDebugButton(string label, UnityEngine.Events.UnityAction action, Vector2 position) {
GameObject buttonObject = CreateUiObject("Debug Button " + label, overlayRoot, typeof(Image), typeof(Button));
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0.5f, 0.5f);
buttonRect.anchorMax = new Vector2(0.5f, 0.5f);
buttonRect.pivot = new Vector2(0.5f, 0.5f);
buttonRect.anchoredPosition = position;
buttonRect.sizeDelta = new Vector2(310f, 34f);
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.color = new Color(0.92f, 0.96f, 1f, 0.94f);
buttonImage.raycastTarget = true;
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = buttonImage;
button.navigation = new Navigation {
mode = Navigation.Mode.None
};
button.onClick.AddListener(action);
TextMeshProUGUI buttonText = AddTextTo(buttonObject.transform, "Label", label, 14f, FontStyles.Bold);
RectTransform textRect = buttonText.rectTransform;
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
buttonText.color = new Color(0.04f, 0.07f, 0.09f, 1f);
}
private void ShowDebugIntroPreview() {
ShowIntro();
}
private void ShowDebugGameStartPreview() {
EnsureDebugPreviewData();
LoadGameSceneForCurrentMap();
}
private void ShowDebugMapGameStartPreview(MapId mapId) {
SetDebugCurrentMap(mapId);
LoadGameSceneForCurrentMap();
}
private void ShowDebugResultPreview(bool cleared) {
EnsureDebugPreviewData();
gameplayActive = false;
pendingStartGameplay = false;
currentScreen = FlowScreen.Result;
completedMap = currentMap;
lastResultCleared = cleared;
lastResultScore = cleared ? 12800 : 7200;
lastResultMileage = cleared ? 263 : 84;
lastResultTime = cleared ? 23.9f : 38.4f;
lastResultDistance = completedMap != null
? (cleared ? completedMap.targetDistance : completedMap.targetDistance * 0.62f)
: 380f;
if(saveData != null)
{
saveData.totalMileage = Mathf.Max(saveData.totalMileage, 4120);
}
ShowResult();
}
private void ShowDebugMapResultPreview(MapId mapId, bool cleared) {
SetDebugCurrentMap(mapId);
gameplayActive = false;
pendingStartGameplay = false;
currentScreen = FlowScreen.Result;
completedMap = currentMap;
lastResultCleared = cleared;
int mapIndex = MapDatabase.GetIndex(mapId);
lastResultScore = cleared ? 12800 + (mapIndex * 2200) : 7200 + (mapIndex * 900);
lastResultMileage = cleared ? 190 + (mapIndex * 70) : 84;
lastResultTime = currentMap != null
? Mathf.Max(10f, currentMap.timeLimit * (cleared ? 0.78f : 1.08f))
: cleared ? 23.9f : 38.4f;
lastResultDistance = currentMap != null
? (cleared ? currentMap.targetDistance : currentMap.targetDistance * 0.62f)
: 380f;
if(saveData != null)
{
saveData.totalMileage = Mathf.Max(saveData.totalMileage, 4120);
}
ShowResult();
}
private void ShowDebugDutyFreeShopPreview() {
EnsureDebugPreviewData();
if(saveData != null)
{
saveData.totalMileage = Mathf.Max(saveData.totalMileage, 4120);
}
ShowDutyFreeShop();
}
private void ShowDebugMapPreview() {
EnsureDebugPreviewData();
ShowMapPreview();
}
private void SetDebugCurrentMap(MapId mapId) {
EnsureDebugPreviewData();
currentMap = MapDatabase.GetMap(mapId);
if(saveData == null)
{
return;
}
int mapIndex = MapDatabase.GetIndex(mapId);
saveData.currentMapIndex = mapIndex;
saveData.unlockedMapIndex = Mathf.Max(saveData.unlockedMapIndex, mapIndex);
GameSaveManager.Save(saveData);
}
private void EnsureDebugPreviewData() {
if(saveData == null)
{
saveData = GameSaveManager.Load();
}
if(currentMap == null)
{
currentMap = MapDatabase.GetMapByIndex(saveData != null ? saveData.currentMapIndex : 0);
}
if(currentMap == null)
{
currentMap = MapDatabase.GetMapByIndex(0);
}
}
#endif
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc38b07e718f43dda9e26895c85dbf3f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,390 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public partial class GameFlowManager {
private void ShowDutyFreeShop() {
currentScreen = FlowScreen.DutyFreeShop;
gameplayActive = false;
pendingStartGameplay = false;
if(TryLoadFlowScene(DutyFreeShopSceneName))
{
return;
}
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
BuildDutyFreeShopScreen();
}
private void BuildDutyFreeShopScreen() {
EnsureDutyFreeStagePurchaseLimits();
ApplyDutyFreeShopBackground();
RectTransform boardRect = AddDutyFreeBoard();
AddDutyFreeHeader(boardRect);
AddDutyFreeItemCard(
boardRect,
"도시락",
DutyFreeItemType.Lunchbox,
LunchboxCost,
saveData.lunchboxStock,
LoadUiSprite("", DutyFreeLunchboxAssetPath),
BuyLunchbox,
new Vector2(75f, -135f));
AddDutyFreeSupplementCard(
boardRect,
"영양제",
SupplementCost,
saveData.supplementPurchases,
LoadUiSprite("", DutyFreeSupplementAssetPath),
BuySupplement,
new Vector2(303f, -135f));
AddDutyFreeItemCard(
boardRect,
"마일리지 카드",
DutyFreeItemType.MileageCard,
MileageCardCost,
saveData.mileageCardStock,
LoadUiSprite("", DutyFreeMileageCardAssetPath),
BuyMileageCard,
new Vector2(760f, -135f));
AddDutyFreeNextTravelButton();
}
private void ApplyDutyFreeShopBackground() {
if(overlayBackground != null)
{
overlayBackground.sprite = null;
overlayBackground.preserveAspect = false;
overlayBackground.color = new Color(0.018f, 0.041f, 0.052f, 0.98f);
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.018f, 0.041f, 0.052f, 1f);
}
AddDutyFreeBackdropImage(
"Duty Free Shop Interior",
LoadUiSprite(DutyFreeBackgroundResourcePath, DutyFreeBackgroundAssetPath),
Vector2.zero,
new Vector2(1378f, 724f),
true,
Color.white);
GameObject scrimObject = CreateUiObject("Duty Free Background Scrim", overlayRoot, typeof(Image));
RectTransform scrimRect = scrimObject.GetComponent<RectTransform>();
scrimRect.anchorMin = Vector2.zero;
scrimRect.anchorMax = Vector2.one;
scrimRect.offsetMin = Vector2.zero;
scrimRect.offsetMax = Vector2.zero;
Image scrimImage = scrimObject.GetComponent<Image>();
scrimImage.color = new Color(0.01f, 0.012f, 0.014f, 0.22f);
scrimImage.raycastTarget = false;
}
private Image AddDutyFreeBackdropImage(string objectName, Sprite sprite, Vector2 position, Vector2 size, bool preserveAspect, Color color) {
GameObject imageObject = CreateUiObject(objectName, overlayRoot, typeof(Image));
RectTransform imageRect = imageObject.GetComponent<RectTransform>();
imageRect.anchorMin = new Vector2(0.5f, 0.5f);
imageRect.anchorMax = new Vector2(0.5f, 0.5f);
imageRect.pivot = new Vector2(0.5f, 0.5f);
imageRect.anchoredPosition = position;
imageRect.sizeDelta = size;
Image image = imageObject.GetComponent<Image>();
image.sprite = sprite;
image.color = sprite != null ? color : new Color(0.30f, 0.46f, 0.54f, 0.28f);
image.preserveAspect = preserveAspect;
image.raycastTarget = false;
return image;
}
private RectTransform AddDutyFreeBoard() {
GameObject boardObject = CreateUiObject("Duty Free Product Board", overlayRoot, typeof(Image));
RectTransform boardRect = boardObject.GetComponent<RectTransform>();
boardRect.anchorMin = new Vector2(0.5f, 0.5f);
boardRect.anchorMax = new Vector2(0.5f, 0.5f);
boardRect.pivot = new Vector2(0.5f, 0.5f);
boardRect.anchoredPosition = new Vector2(0f, 20f);
boardRect.sizeDelta = new Vector2(1038f, 552f);
Image boardImage = boardObject.GetComponent<Image>();
boardImage.sprite = LoadUiSprite(DutyFreeWindowPanelResourcePath, DutyFreeWindowPanelAssetPath);
boardImage.color = boardImage.sprite != null
? Color.white
: new Color(0.92f, 0.98f, 1f, 0.78f);
boardImage.preserveAspect = false;
boardImage.raycastTarget = false;
return boardRect;
}
private void AddDutyFreeHeader(RectTransform parent) {
AddResultImage(parent, "Duty Free Equip Slots Panel", LoadUiSprite(DutyFreeEquipSlotPanelResourcePath, "Assets/Resources/UI_DutyFreeBagSlotPanel.png"), new Vector2(50f, -570f), new Vector2(276f, 124f), true, Color.white);
for(int i = 0; i < 3; i++)
{
AddDutyFreeEquipSlot(parent, i, new Vector2(-65f + (i * 82f), -540f));
}
AddDutyFreeBagUpgradeButton(parent, new Vector2(190f, -560f));
AddResultImage(parent, "Duty Free Mileage Price Tag", LoadDutyFreeResourceSprite(DutyFreePriceTagResourcePath), new Vector2(1000f, -48f), new Vector2(230f, 100f), false, Color.white);
AddResultText(parent, "Duty Free Mileage Label", "보유 마일리지", new Vector2(990f, -26f), new Vector2(118f, 20f), 14f, FontStyles.Bold, new Color(0.94f, 0.99f, 1f, 0.96f), TextAlignmentOptions.MidlineLeft);
AddResultText(parent, "Duty Free Mileage Value", saveData.totalMileage + " M", new Vector2(1000f, -45f), new Vector2(122f, 28f), 21f, FontStyles.Bold, new Color(1f, 0.90f, 0.38f, 1f), TextAlignmentOptions.MidlineLeft);
}
private void AddDutyFreeEquipSlot(RectTransform parent, int slotIndex, Vector2 topLeftPosition) {
bool unlocked = slotIndex < saveData.bagSlots;
DutyFreeItemType itemType = unlocked ? GetEquippedDutyFreeItem(slotIndex) : DutyFreeItemType.None;
bool hasItem = itemType != DutyFreeItemType.None;
GameObject slotObject = CreateUiObject("Duty Free Equip Slot " + (slotIndex + 1), parent, typeof(Image), typeof(Button));
RectTransform slotRect = slotObject.GetComponent<RectTransform>();
slotRect.anchorMin = new Vector2(0f, 1f);
slotRect.anchorMax = new Vector2(0f, 1f);
slotRect.pivot = new Vector2(0f, 1f);
slotRect.anchoredPosition = topLeftPosition;
slotRect.sizeDelta = new Vector2(64f, 64f);
Image slotImage = slotObject.GetComponent<Image>();
if(hasItem)
{
slotImage.sprite = LoadDutyFreeResourceSprite(DutyFreeOccupiedSlotResourcePath);
slotImage.color = slotImage.sprite != null ? Color.white : new Color(0.05f, 0.16f, 0.20f, 0.88f);
}
else if(unlocked)
{
slotImage.sprite = LoadDutyFreeResourceSprite(DutyFreeActiveSlotResourcePath);
slotImage.color = slotImage.sprite != null ? Color.white : new Color(0.58f, 0.36f, 0.18f, 0.88f);
}
else
{
slotImage.sprite = LoadDutyFreeResourceSprite(DutyFreeInactiveSlotResourcePath);
slotImage.color = slotImage.sprite != null ? Color.white : new Color(0.35f, 0.35f, 0.35f, 0.78f);
}
slotImage.preserveAspect = true;
slotImage.raycastTarget = hasItem;
Button slotButton = slotObject.GetComponent<Button>();
slotButton.targetGraphic = slotImage;
slotButton.interactable = hasItem;
slotButton.navigation = new Navigation {
mode = Navigation.Mode.None
};
if(hasItem)
{
int capturedSlotIndex = slotIndex;
slotButton.onClick.AddListener(delegate { UnequipDutyFreeSlot(capturedSlotIndex); });
}
AddResultText(slotRect, "Duty Free Slot Number", (slotIndex + 1).ToString(), new Vector2(3f, -3f), new Vector2(18f, 18f), 10f, FontStyles.Bold, new Color(0.78f, 0.92f, 0.97f, 0.94f), TextAlignmentOptions.Center);
if(hasItem)
{
AddResultImage(slotRect, "Duty Free Slot Icon", GetDutyFreeItemSprite(itemType), new Vector2(32f, -31f), new Vector2(36f, 36f), true, Color.white);
AddResultText(slotRect, "Duty Free Slot Name", GetDutyFreeItemShortName(itemType), new Vector2(5f, -50f), new Vector2(54f, 12f), 8f, FontStyles.Bold, new Color(0.94f, 0.99f, 1f, 0.96f), TextAlignmentOptions.Center);
}
}
private void AddDutyFreeBagUpgradeButton(RectTransform parent, Vector2 topLeftPosition) {
bool isMax = saveData.bagSlots >= 3;
bool canBuy = !isMax && saveData.totalMileage >= BagSlotUpgradeCost;
GameObject buttonObject = CreateUiObject("Duty Free Bag Upgrade", parent, typeof(Image), typeof(Button));
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0f, 1f);
buttonRect.anchorMax = new Vector2(0f, 1f);
buttonRect.pivot = new Vector2(0f, 1f);
buttonRect.anchoredPosition = topLeftPosition;
buttonRect.sizeDelta = new Vector2(160f, 64f);
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.sprite = LoadUiSprite(
canBuy ? DutyFreeButtonPrimaryResourcePath : DutyFreeButtonDisabledResourcePath,
canBuy ? "Assets/Resources/UI_DutyFreeButton_PrimaryYellow.png" : "Assets/Resources/UI_DutyFreeButton_DisabledGray.png");
buttonImage.color = canBuy
? Color.white
: (buttonImage.sprite != null ? Color.white : new Color(0.36f, 0.43f, 0.47f, 0.76f));
buttonImage.preserveAspect = false;
buttonImage.raycastTarget = canBuy;
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = buttonImage;
button.interactable = canBuy;
button.navigation = new Navigation {
mode = Navigation.Mode.None
};
button.onClick.AddListener(BuyBagSlotUpgrade);
AddResultImage(buttonRect, "Duty Free Bag Upgrade Icon", LoadUiSprite("", DutyFreeBackpackAssetPath), new Vector2(45f, -32f), new Vector2(42f, 42f), true, canBuy ? Color.white : new Color(1f, 1f, 1f, 0.48f));
AddResultText(buttonRect, "Duty Free Bag Upgrade Label", isMax ? "가방 MAX" : "가방 +1", new Vector2(75f, -11f), new Vector2(66f, 24f), 16f, FontStyles.Bold, canBuy ? new Color(0.06f, 0.16f, 0.22f, 1f) : new Color(0.72f, 0.80f, 0.84f, 0.92f), TextAlignmentOptions.MidlineLeft);
AddResultText(buttonRect, "Duty Free Bag Upgrade Cost", isMax ? "" : BagSlotUpgradeCost + " M", new Vector2(85f, -36f), new Vector2(66f, 18f), 14f, FontStyles.Bold, canBuy ? new Color(0.87f, 0.55f, 0.08f, 1f) : new Color(0.62f, 0.68f, 0.70f, 0.92f), TextAlignmentOptions.MidlineLeft);
}
private void AddDutyFreeItemCard(RectTransform parent, string itemName, DutyFreeItemType itemType, int cost, int stock, Sprite itemSprite, UnityEngine.Events.UnityAction buyAction, Vector2 topLeftPosition) {
bool canBuy = CanBuyDutyFreeItem(itemType, cost);
bool hasFreeSlot = HasFreeDutyFreeSlot();
int equippedCount = GetEquippedDutyFreeItemCount(itemType);
bool canEquip = stock > equippedCount && hasFreeSlot;
bool canInteract = canEquip || canBuy;
GameObject cardObject = CreateUiObject("Duty Free Item " + itemName, parent, typeof(Image));
RectTransform cardRect = cardObject.GetComponent<RectTransform>();
cardRect.anchorMin = new Vector2(0f, 1f);
cardRect.anchorMax = new Vector2(0f, 1f);
cardRect.pivot = new Vector2(0f, 1f);
cardRect.anchoredPosition = topLeftPosition;
cardRect.sizeDelta = new Vector2(206f, 288f);
Image cardImage = cardObject.GetComponent<Image>();
cardImage.sprite = GetDutyFreeProductCardSprite(itemType);
cardImage.color = cardImage.sprite != null
? (canInteract ? Color.white : new Color(0.68f, 0.70f, 0.72f, 0.92f))
: canInteract
? new Color(0.97f, 0.91f, 0.78f, 0.96f)
: new Color(0.40f, 0.45f, 0.48f, 0.88f);
cardImage.preserveAspect = false;
cardImage.raycastTarget = false;
Color textColor = canInteract
? new Color(0.06f, 0.16f, 0.22f, 1f)
: new Color(0.78f, 0.84f, 0.86f, 1f);
AddResultImage(cardRect, "Duty Free " + itemName + " Image", itemSprite, new Vector2(103f, -107f), new Vector2(92f, 96f), true, canInteract ? Color.white : new Color(1f, 1f, 1f, 0.54f));
Image priceTagImage = AddResultImage(cardRect, "Duty Free " + itemName + " Price Tag", LoadDutyFreeResourceSprite(DutyFreeProductPriceTagResourcePath), new Vector2(151f, -150f), new Vector2(52f, 78f), true, canInteract ? Color.white : new Color(1f, 1f, 1f, 0.62f));
priceTagImage.rectTransform.localEulerAngles = new Vector3(0f, 0f, 110f);
TextMeshProUGUI priceText = AddResultCenteredText(cardRect, "Duty Free " + itemName + " Price", cost + "M", new Vector2(153f, -150f), new Vector2(40f, 18f), 11f, FontStyles.Bold, canInteract ? new Color(0.03f, 0.03f, 0.028f, 1f) : new Color(0.36f, 0.36f, 0.34f, 0.92f), TextAlignmentOptions.Center);
priceText.rectTransform.pivot = new Vector2(0.5f, 0.5f);
priceText.rectTransform.anchoredPosition = new Vector2(155f, -150f);
priceText.rectTransform.localEulerAngles = new Vector3(0f, 0f, 20f);
TextMeshProUGUI nameText = AddResultCenteredText(cardRect, "Duty Free " + itemName + " Name", itemName + "\n" + GetDutyFreeItemEffectText(itemType), new Vector2(103f, -199f), new Vector2(126f, 34f), 13f, FontStyles.Bold, textColor, TextAlignmentOptions.Center);
nameText.lineSpacing = -8f;
GameObject buyObject = CreateUiObject("Duty Free " + itemName + " Buy Area", cardRect, typeof(Image), typeof(Button));
RectTransform buyRect = buyObject.GetComponent<RectTransform>();
buyRect.anchorMin = new Vector2(0f, 1f);
buyRect.anchorMax = new Vector2(0f, 1f);
buyRect.pivot = new Vector2(0f, 1f);
buyRect.anchoredPosition = new Vector2(39f, -232f);
buyRect.sizeDelta = new Vector2(128f, 22f);
Image buyImage = buyObject.GetComponent<Image>();
buyImage.color = Color.clear;
buyImage.raycastTarget = canInteract;
Button buyButton = buyObject.GetComponent<Button>();
buyButton.targetGraphic = buyImage;
buyButton.interactable = canInteract;
buyButton.transition = Selectable.Transition.None;
buyButton.navigation = new Navigation {
mode = Navigation.Mode.None
};
buyButton.onClick.AddListener(buyAction);
TextMeshProUGUI buyText = AddResultText(buyRect, "Duty Free " + itemName + " Buy Label", GetDutyFreePurchaseLabel(itemType, canEquip, canBuy, hasFreeSlot, stock, equippedCount), Vector2.zero, new Vector2(128f, 22f), 11f, FontStyles.Bold, new Color(0.94f, 0.99f, 1f, 1f), TextAlignmentOptions.Center);
buyText.outlineColor = new Color(0f, 0.04f, 0.07f, 0.98f);
buyText.outlineWidth = 0.12f;
}
private void AddDutyFreeSupplementCard(RectTransform parent, string itemName, int cost, int purchaseCount, Sprite itemSprite, UnityEngine.Events.UnityAction buyAction, Vector2 topLeftPosition) {
bool isMax = purchaseCount >= MaxSupplementPurchases;
bool alreadyPurchasedThisStage = HasPurchasedSupplementInDutyFreeStage();
bool canBuy = CanBuySupplement();
GameObject cardObject = CreateUiObject("Duty Free Item " + itemName, parent, typeof(Image));
RectTransform cardRect = cardObject.GetComponent<RectTransform>();
cardRect.anchorMin = new Vector2(0f, 1f);
cardRect.anchorMax = new Vector2(0f, 1f);
cardRect.pivot = new Vector2(0f, 1f);
cardRect.anchoredPosition = topLeftPosition;
cardRect.sizeDelta = new Vector2(206f, 288f);
Image cardImage = cardObject.GetComponent<Image>();
cardImage.sprite = GetDutyFreeProductCardSprite(DutyFreeItemType.Supplement);
cardImage.color = cardImage.sprite != null
? (canBuy || isMax ? Color.white : new Color(0.68f, 0.70f, 0.72f, 0.92f))
: canBuy
? new Color(0.97f, 0.91f, 0.78f, 0.96f)
: new Color(0.40f, 0.45f, 0.48f, 0.88f);
cardImage.preserveAspect = false;
cardImage.raycastTarget = false;
Color textColor = canBuy || isMax
? new Color(0.06f, 0.16f, 0.22f, 1f)
: new Color(0.78f, 0.84f, 0.86f, 1f);
AddResultImage(cardRect, "Duty Free " + itemName + " Image", itemSprite, new Vector2(103f, -107f), new Vector2(92f, 96f), true, canBuy || isMax ? Color.white : new Color(1f, 1f, 1f, 0.54f));
Image priceTagImage = AddResultImage(cardRect, "Duty Free " + itemName + " Price Tag", LoadDutyFreeResourceSprite(DutyFreeProductPriceTagResourcePath), new Vector2(151f, -150f), new Vector2(52f, 78f), true, canBuy || isMax ? Color.white : new Color(1f, 1f, 1f, 0.62f));
priceTagImage.rectTransform.localEulerAngles = new Vector3(0f, 0f, 110f);
TextMeshProUGUI priceText = AddResultCenteredText(cardRect, "Duty Free " + itemName + " Price", isMax ? "MAX" : cost + "M", new Vector2(153f, -150f), new Vector2(40f, 18f), 11f, FontStyles.Bold, canBuy || isMax ? new Color(0.03f, 0.03f, 0.028f, 1f) : new Color(0.36f, 0.36f, 0.34f, 0.92f), TextAlignmentOptions.Center);
priceText.rectTransform.pivot = new Vector2(0.5f, 0.5f);
priceText.rectTransform.anchoredPosition = new Vector2(155f, -150f);
priceText.rectTransform.localEulerAngles = new Vector3(0f, 0f, 20f);
TextMeshProUGUI nameText = AddResultCenteredText(cardRect, "Duty Free " + itemName + " Name", itemName + "\n" + GetDutyFreeSupplementEffectText(purchaseCount), new Vector2(103f, -199f), new Vector2(126f, 34f), 13f, FontStyles.Bold, textColor, TextAlignmentOptions.Center);
nameText.lineSpacing = -8f;
GameObject buyObject = CreateUiObject("Duty Free " + itemName + " Buy Area", cardRect, typeof(Image), typeof(Button));
RectTransform buyRect = buyObject.GetComponent<RectTransform>();
buyRect.anchorMin = new Vector2(0f, 1f);
buyRect.anchorMax = new Vector2(0f, 1f);
buyRect.pivot = new Vector2(0f, 1f);
buyRect.anchoredPosition = new Vector2(39f, -232f);
buyRect.sizeDelta = new Vector2(128f, 22f);
Image buyImage = buyObject.GetComponent<Image>();
buyImage.color = Color.clear;
buyImage.raycastTarget = canBuy;
Button buyButton = buyObject.GetComponent<Button>();
buyButton.targetGraphic = buyImage;
buyButton.interactable = canBuy;
buyButton.transition = Selectable.Transition.None;
buyButton.navigation = new Navigation {
mode = Navigation.Mode.None
};
buyButton.onClick.AddListener(buyAction);
TextMeshProUGUI buyText = AddResultText(buyRect, "Duty Free " + itemName + " Buy Label", GetDutyFreeSupplementPurchaseLabel(canBuy, isMax, alreadyPurchasedThisStage), Vector2.zero, new Vector2(128f, 22f), 11f, FontStyles.Bold, new Color(0.94f, 0.99f, 1f, 1f), TextAlignmentOptions.Center);
buyText.outlineColor = new Color(0f, 0.04f, 0.07f, 0.98f);
buyText.outlineWidth = 0.12f;
}
private void AddDutyFreeNextTravelButton() {
GameObject buttonObject = CreateUiObject("Duty Free Next Travel Sign", overlayRoot, typeof(Image), typeof(Button));
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0.5f, 0.5f);
buttonRect.anchorMax = new Vector2(0.5f, 0.5f);
buttonRect.pivot = new Vector2(0.5f, 0.5f);
buttonRect.anchoredPosition = new Vector2(548f, -247f);
buttonRect.sizeDelta = new Vector2(200f, 160f);
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.sprite = LoadDutyFreeResourceSprite(DutyFreeDirectionSignResourcePath);
buttonImage.color = buttonImage.sprite != null ? Color.white : new Color(0.82f, 0.91f, 0.96f, 0.90f);
buttonImage.preserveAspect = true;
buttonImage.raycastTarget = true;
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = buttonImage;
button.onClick.AddListener(ShowMapPreview);
button.navigation = new Navigation {
mode = Navigation.Mode.None
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8edc62b4b60248ce8001165fb0588bf8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,377 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public partial class GameFlowManager {
private void ShowResult() {
if(TryLoadFlowScene(ResultSceneName))
{
return;
}
EnsureOverlay();
ClearOverlay();
SetOverlayVisible(true);
BuildResultScreen();
}
private void BuildResultScreen() {
ApplyResultAirportBackground();
MapDefinition resultMap = completedMap != null ? completedMap : currentMap;
int rewardMileage = lastResultCleared && resultMap != null ? resultMap.clearMileageReward : 0;
int gainedMileage = Mathf.Max(0, lastResultMileage) + rewardMileage;
int targetDistance = resultMap != null ? Mathf.FloorToInt(resultMap.targetDistance) : 0;
int shownDistance = targetDistance > 0
? Mathf.Clamp(Mathf.FloorToInt(lastResultDistance), 0, targetDistance)
: Mathf.Max(0, Mathf.FloorToInt(lastResultDistance));
RectTransform passportRect = AddResultPassportPanel();
AddResultHeader(passportRect, resultMap);
AddResultStats(passportRect, gainedMileage, shownDistance, targetDistance);
AddResultTickets(passportRect, resultMap, rewardMileage);
if(lastResultCleared)
{
AddResultActionButton("면세점으로", ShowDutyFreeShop, new Vector2(-166f, -282f), new Vector2(250f, 48f), true, LoadUiSprite("", DutyFreeIconAssetPath));
AddResultActionButton("다음 여행지 가기", ShowMapPreview, new Vector2(160f, -282f), new Vector2(292f, 48f), false, LoadUiSprite("", AirplaneTravelIconAssetPath));
}
else
{
AddResultActionButton("다시 도전", RetryCurrentMap, new Vector2(-166f, -282f), new Vector2(250f, 48f), true, LoadUiSprite("", CheckpointIconAssetPath));
AddResultActionButton("다음 여행지 가기", ShowMapPreview, new Vector2(160f, -282f), new Vector2(292f, 48f), false, LoadUiSprite("", AirplaneTravelIconAssetPath));
}
}
private void ApplyResultAirportBackground() {
if(overlayBackground != null)
{
overlayBackground.sprite = LoadUiSprite(ResultAirportBackgroundResourcePath, "Assets/Resources/Map_Tutorial_Incheon_Airport.png");
overlayBackground.preserveAspect = false;
overlayBackground.color = overlayBackground.sprite != null
? Color.white
: new Color(0.008f, 0.024f, 0.034f, 0.96f);
}
if(flowCamera != null)
{
flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f);
}
GameObject scrimObject = CreateUiObject("Result Airport Background Scrim", overlayRoot, typeof(Image));
RectTransform scrimRect = scrimObject.GetComponent<RectTransform>();
scrimRect.anchorMin = Vector2.zero;
scrimRect.anchorMax = Vector2.one;
scrimRect.offsetMin = Vector2.zero;
scrimRect.offsetMax = Vector2.zero;
Image scrimImage = scrimObject.GetComponent<Image>();
scrimImage.color = new Color(0.005f, 0.015f, 0.020f, 0.30f);
scrimImage.raycastTarget = false;
}
private RectTransform AddResultPassportPanel() {
GameObject panelObject = CreateUiObject("Result Passport Page", overlayRoot, typeof(Image));
RectTransform panelRect = panelObject.GetComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.pivot = new Vector2(0.5f, 0.5f);
panelRect.anchoredPosition = new Vector2(0f, 38f);
panelRect.sizeDelta = new Vector2(1092f, 330f);
Image panelImage = panelObject.GetComponent<Image>();
panelImage.sprite = LoadUiSprite(ResultPassportPanelResourcePath, ResultPassportPanelAssetPath);
panelImage.color = panelImage.sprite != null
? Color.white
: new Color(0.95f, 0.91f, 0.80f, 0.98f);
panelImage.preserveAspect = true;
panelImage.raycastTarget = false;
return panelRect;
}
private void AddResultHeader(RectTransform parent, MapDefinition resultMap) {
Sprite rankSprite = LoadUiSprite("", ClearRankIconAssetPath);
AddResultImage(parent, "Result Rank Badge", rankSprite, new Vector2(150f, -140f), new Vector2(48f, 48f), true, Color.white);
string title = lastResultCleared ? "출국 성공" : "일정 실패";
Color titleColor = lastResultCleared
? new Color(0.05f, 0.17f, 0.27f, 1f)
: new Color(0.52f, 0.08f, 0.07f, 1f);
AddResultCenteredText(parent, "Result Title", title, new Vector2(244f, -140f), new Vector2(220f, 50f), 25f, FontStyles.Bold, titleColor, TextAlignmentOptions.Center);
AddResultRouteBox(parent, "From", GetResultOriginAirportCode(resultMap), GetResultOriginCityName(resultMap), new Vector2(440f, -122f));
AddResultRouteBox(parent, "To", GetResultDestinationAirportCode(resultMap), GetResultDestinationCityName(resultMap), new Vector2(660f, -122f));
AddResultBoardingField(parent, "Flight", "항공편", GetResultFlightName(resultMap), new Vector2(400f, -253f));
AddResultBoardingField(parent, "Seat", "좌석", GetResultSeatName(resultMap), new Vector2(497f, -253f));
AddResultBoardingField(parent, "Boarding", "탑승", System.DateTime.Now.ToString("HH:mm"), new Vector2(595f, -253f));
AddResultBoardingField(parent, "Class", "클래스", lastResultCleared ? "일반석" : "대기", new Vector2(695f, -253f));
}
private void AddResultStats(RectTransform parent, int gainedMileage, int shownDistance, int targetDistance) {
AddResultStat(parent, "Mileage", LoadUiSprite("", MileageIconAssetPath), "마일리지", "+" + gainedMileage, new Vector2(812f, -140f));
AddResultStat(parent, "Time", LoadUiSprite("", TimerIconAssetPath), "도착 시간", lastResultTime.ToString("0.0") + "s", new Vector2(903f, -140f));
string distance = targetDistance > 0
? shownDistance + "/" + targetDistance + "m"
: shownDistance + "m";
AddResultStat(parent, "Distance", LoadUiSprite("", CheckpointIconAssetPath), "이동 거리", distance, new Vector2(993f, -140f));
}
private void AddResultTickets(RectTransform parent, MapDefinition resultMap, int rewardMileage) {
AddResultTicket(
parent,
"Destination",
LoadUiSprite("", CheckpointIconAssetPath),
"목적지",
GetResultDestinationCityName(resultMap),
new Vector2(812f, -251f));
AddResultTicket(
parent,
"Souvenir",
lastResultCleared ? GetResultSouvenirSprite(resultMap) : LoadUiSprite("", SouvenirCollectionIconAssetPath),
"보상",
lastResultCleared ? GetResultSouvenirName(resultMap) : "다음 도전",
new Vector2(900f, -251f));
AddResultTicket(
parent,
"Total Mileage",
LoadUiSprite("", MileageIconAssetPath),
"보유",
saveData.totalMileage + " M",
new Vector2(991f, -251f));
}
private void AddResultStat(RectTransform parent, string objectName, Sprite iconSprite, string label, string value, Vector2 centerPosition) {
AddResultImage(parent, "Result " + objectName + " Icon", iconSprite, centerPosition + new Vector2(0f, 5f), new Vector2(26f, 26f), true, Color.white);
AddResultCenteredText(parent, "Result " + objectName + " Value", value, centerPosition + new Vector2(0f, -17f), new Vector2(90f, 22f), 12f, FontStyles.Bold, new Color(0.05f, 0.17f, 0.25f, 1f), TextAlignmentOptions.Center);
AddResultCenteredText(parent, "Result " + objectName + " Label", label, centerPosition + new Vector2(0f, -32f), new Vector2(90f, 16f), 8f, FontStyles.Bold, new Color(0.42f, 0.58f, 0.64f, 0.92f), TextAlignmentOptions.Center);
}
private void AddResultTicket(RectTransform parent, string objectName, Sprite iconSprite, string label, string value, Vector2 centerPosition) {
AddResultImage(parent, "Result " + objectName + " Icon", iconSprite, centerPosition + new Vector2(0f, 27f), new Vector2(34f, 34f), true, Color.white);
AddResultCenteredText(parent, "Result " + objectName + " Label", label, centerPosition + new Vector2(0f, -9f), new Vector2(110f, 18f), 11f, FontStyles.Bold, new Color(0.43f, 0.58f, 0.64f, 0.96f), TextAlignmentOptions.Center);
AddResultCenteredText(parent, "Result " + objectName + " Value", value, centerPosition + new Vector2(0f, -35f), new Vector2(118f, 26f), 11f, FontStyles.Bold, new Color(0.05f, 0.18f, 0.27f, 1f), TextAlignmentOptions.Center);
}
private void AddResultRouteBox(RectTransform parent, string objectName, string code, string cityName, Vector2 centerPosition) {
AddResultCenteredText(parent, "Result " + objectName + " Airport Code", code, centerPosition + new Vector2(0f, 7f), new Vector2(150f, 48f), 34f, FontStyles.Bold, new Color(0.05f, 0.17f, 0.27f, 1f), TextAlignmentOptions.Center);
AddResultCenteredText(parent, "Result " + objectName + " City", cityName, centerPosition + new Vector2(0f, -30f), new Vector2(160f, 22f), 12f, FontStyles.Bold, new Color(0.43f, 0.58f, 0.64f, 0.96f), TextAlignmentOptions.Center);
}
private void AddResultBoardingField(RectTransform parent, string objectName, string label, string value, Vector2 centerPosition) {
AddResultCenteredText(parent, "Result " + objectName + " Label", label, centerPosition + new Vector2(0f, 13f), new Vector2(104f, 17f), 9f, FontStyles.Bold, new Color(0.43f, 0.58f, 0.64f, 0.96f), TextAlignmentOptions.Center);
AddResultCenteredText(parent, "Result " + objectName + " Value", value, centerPosition + new Vector2(0f, -12f), new Vector2(108f, 28f), 14f, FontStyles.Bold, new Color(0.05f, 0.18f, 0.27f, 1f), TextAlignmentOptions.Center);
}
private void AddResultActionButton(string label, UnityEngine.Events.UnityAction action, Vector2 position, Vector2 size, bool primary, Sprite iconSprite) {
GameObject buttonObject = CreateUiObject("Result Button " + label, overlayRoot, typeof(Image), typeof(Button));
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
buttonRect.anchorMin = new Vector2(0.5f, 0.5f);
buttonRect.anchorMax = new Vector2(0.5f, 0.5f);
buttonRect.pivot = new Vector2(0.5f, 0.5f);
buttonRect.anchoredPosition = position;
buttonRect.sizeDelta = size;
Image buttonImage = buttonObject.GetComponent<Image>();
buttonImage.sprite = LoadUiSprite(ResultButtonResourcePath, ResultButtonAssetPath);
buttonImage.color = buttonImage.sprite != null
? (primary ? Color.white : new Color(0.86f, 0.94f, 1f, 0.94f))
: primary
? new Color(1f, 0.82f, 0.28f, 0.98f)
: new Color(0.82f, 0.91f, 0.96f, 0.90f);
buttonImage.preserveAspect = false;
buttonImage.raycastTarget = true;
Button button = buttonObject.GetComponent<Button>();
button.onClick.AddListener(action);
if(iconSprite != null)
{
AddResultImage(buttonRect, "Result Button Icon", iconSprite, new Vector2(34f, -(size.y * 0.5f)), new Vector2(24f, 24f), true, Color.white);
}
TextMeshProUGUI buttonText = AddResultText(buttonRect, "Result Button Label", label, new Vector2(iconSprite != null ? 54f : 0f, -1f), new Vector2(iconSprite != null ? size.x - 70f : size.x, size.y), 16f, FontStyles.Bold, new Color(0.94f, 0.99f, 1f, 1f), TextAlignmentOptions.Center);
RectTransform textRect = buttonText.rectTransform;
textRect.anchorMin = new Vector2(0f, 0.5f);
textRect.anchorMax = new Vector2(0f, 0.5f);
textRect.pivot = new Vector2(0f, 0.5f);
buttonText.outlineColor = new Color(0f, 0.04f, 0.07f, 0.98f);
buttonText.outlineWidth = 0.12f;
}
private Image AddResultImage(RectTransform parent, string objectName, Sprite sprite, Vector2 position, Vector2 size, bool preserveAspect, Color color) {
GameObject imageObject = CreateUiObject(objectName, parent, typeof(Image));
RectTransform imageRect = imageObject.GetComponent<RectTransform>();
imageRect.anchorMin = new Vector2(0f, 1f);
imageRect.anchorMax = new Vector2(0f, 1f);
imageRect.pivot = new Vector2(0.5f, 0.5f);
imageRect.anchoredPosition = position;
imageRect.sizeDelta = size;
Image image = imageObject.GetComponent<Image>();
image.sprite = sprite;
image.color = sprite != null ? color : new Color(0.30f, 0.46f, 0.54f, 0.28f);
image.preserveAspect = preserveAspect;
image.raycastTarget = false;
return image;
}
private TextMeshProUGUI AddResultText(RectTransform parent, string objectName, string text, Vector2 position, Vector2 size, float fontSize, FontStyles fontStyle, Color color, TextAlignmentOptions alignment) {
GameObject textObject = CreateUiObject(objectName, parent);
RectTransform textRect = textObject.GetComponent<RectTransform>();
textRect.anchorMin = new Vector2(0f, 1f);
textRect.anchorMax = new Vector2(0f, 1f);
textRect.pivot = new Vector2(0f, 1f);
textRect.anchoredPosition = position;
textRect.sizeDelta = size;
TextMeshProUGUI textComponent = textObject.AddComponent<TextMeshProUGUI>();
textComponent.text = text;
textComponent.font = runtimeFont != null ? runtimeFont : textComponent.font;
textComponent.fontSize = fontSize;
textComponent.fontStyle = fontStyle;
textComponent.alignment = alignment;
textComponent.color = color;
textComponent.raycastTarget = false;
textComponent.textWrappingMode = TextWrappingModes.Normal;
textComponent.overflowMode = TextOverflowModes.Ellipsis;
textComponent.enableAutoSizing = true;
textComponent.fontSizeMin = Mathf.Max(8f, fontSize * 0.66f);
textComponent.fontSizeMax = fontSize;
return textComponent;
}
private TextMeshProUGUI AddResultCenteredText(RectTransform parent, string objectName, string text, Vector2 centerPosition, Vector2 size, float fontSize, FontStyles fontStyle, Color color, TextAlignmentOptions alignment) {
Vector2 topLeftPosition = centerPosition + new Vector2(-size.x * 0.5f, size.y * 0.5f);
return AddResultText(parent, objectName, text, topLeftPosition, size, fontSize, fontStyle, color, alignment);
}
private string GetResultOriginAirportCode(MapDefinition resultMap) {
return GetAirportCode(GetResultOriginMapId(resultMap));
}
private string GetResultDestinationAirportCode(MapDefinition resultMap) {
return GetAirportCode(GetResultDestinationMapId(resultMap));
}
private string GetResultOriginCityName(MapDefinition resultMap) {
return GetAirportCityName(GetResultOriginMapId(resultMap));
}
private string GetResultDestinationCityName(MapDefinition resultMap) {
return GetAirportCityName(GetResultDestinationMapId(resultMap));
}
private MapId GetResultOriginMapId(MapDefinition resultMap) {
if(resultMap == null)
{
return MapId.TutorialIncheon;
}
return resultMap.mapId;
}
private MapId GetResultDestinationMapId(MapDefinition resultMap) {
if(resultMap == null)
{
return MapId.Japan;
}
if(resultMap.hasNextMap)
{
return resultMap.nextMapId;
}
return resultMap.mapId;
}
private string GetAirportCode(MapId mapId) {
switch(mapId)
{
case MapId.TutorialIncheon:
return "ICN";
case MapId.Japan:
return "FUK";
case MapId.China:
return "PVG";
case MapId.America:
return "LAX";
default:
return "TRP";
}
}
private string GetAirportCityName(MapId mapId) {
switch(mapId)
{
case MapId.TutorialIncheon:
return "인천공항";
case MapId.Japan:
return "후쿠오카";
case MapId.China:
return "상하이";
case MapId.America:
return "로스앤젤레스";
default:
return "여행지";
}
}
private string GetResultFlightName(MapDefinition resultMap) {
int mapNumber = resultMap != null ? (int)resultMap.mapId + 1 : 1;
return "UNR-" + mapNumber.ToString("000");
}
private string GetResultSeatName(MapDefinition resultMap) {
int mapNumber = resultMap != null ? (int)resultMap.mapId + 1 : 1;
return (10 + mapNumber).ToString() + "A";
}
private string GetResultSouvenirName(MapDefinition resultMap) {
if(resultMap == null)
{
return "여행 도장";
}
switch(resultMap.mapId)
{
case MapId.TutorialIncheon:
return "공항 마그넷";
case MapId.Japan:
return "일본 마그넷";
case MapId.China:
return "여행 부적";
case MapId.America:
return "스카이라인";
default:
return "여행 도장";
}
}
private Sprite GetResultSouvenirSprite(MapDefinition resultMap) {
if(resultMap == null)
{
return LoadUiSprite("", SouvenirCollectionIconAssetPath);
}
switch(resultMap.mapId)
{
case MapId.TutorialIncheon:
return LoadUiSprite("", AirportSouvenirAssetPath);
case MapId.Japan:
return LoadUiSprite("", JapanSouvenirAssetPath);
case MapId.China:
return LoadUiSprite("", ChinaSouvenirAssetPath);
case MapId.America:
return LoadUiSprite("", AmericaSouvenirAssetPath);
default:
return LoadUiSprite("", SouvenirCollectionIconAssetPath);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8bf10c40d6cc4b319e90f0bf9279966b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+370 -82
View File
@@ -22,8 +22,7 @@ public class GameManager : MonoBehaviour {
public enum TutorialItemType {
HealthSushi,
InvincibleRamen,
Shield,
SpeedShoes,
EnergyDrink,
MileageCard
}
@@ -43,6 +42,8 @@ public class GameManager : MonoBehaviour {
private Image rightInfoHudBackground; // 오른쪽 거리/마일리지 수하물 라벨 배경
private TextMeshProUGUI rightMileageText; // 오른쪽 HUD의 적립 마일리지 텍스트
private Image[] lifeIcons; // 왼쪽 HUD의 목숨 도장 표시
private Image[] itemSlotIcons; // 왼쪽 HUD의 장착 아이템 표시
private DutyFreeItemType[] preparedDutyFreeItems; // 면세점에서 장착한 스테이지 준비 아이템
private GameObject tutorialGuideObject; // 화면 상단 이미지 튜토리얼 패널
private Image tutorialMainImage; // 점프/슬라이드 대표 이미지
private TextMeshProUGUI tutorialMultiplierText; // 2단 점프 x2 표시
@@ -74,8 +75,8 @@ public class GameManager : MonoBehaviour {
public bool playTutorialGuideSequence = false; // 코스 데이터가 없을 때만 시간 기준 안내를 보여준다.
public bool pauseGameplayForTutorialGuide = true; // 안내가 뜨는 동안 게임을 잠깐 멈춘다.
public float tutorialGuidePauseDuration = 2f; // 이전 자동 숨김 시간 값입니다. 현재 안내는 확인 클릭으로만 닫습니다.
public float itemInvincibleDuration = 2.2f; // 라멘 아이템 무적 지속 시간
public float itemSpeedBoostAmount = 0.35f; // 운동화 아이템 즉시 속도 보정량
public float itemInvincibleDuration = 2.2f; // 무적 아이템 지속 시간
public float itemSpeedBoostAmount = 0.35f; // 에너지 드링크 즉시 속도 보정량
public float mileageBonusDuration = 7f; // 마일리지 카드 보너스 지속 시간
public int mileageBonusMultiplier = 2; // 마일리지 카드 획득 배율
public float finishGateSpawnX = 20f; // 도착 게이트가 처음 나타나는 x 위치
@@ -86,21 +87,29 @@ public class GameManager : MonoBehaviour {
private const string TotalMileageKey = "UniRun.TotalMileage";
private const string InitialMileageGrantedKey = "UniRun.InitialMileageGranted";
private const string CharacterFaceResourcePath = "UI_JJ_PassportPhoto";
private const string PassportPanelResourcePath = "UI_LeftHudPassportCompactPanel";
private const string PassportPanelResourcePath = "UI_LeftHudPassportHorizontalGridPanel";
private const string PassportPhotoFrameResourcePath = "UI_PassportHudPhotoFrame";
private const string PassportPhotoHeaderResourcePath = "UI_PassportHudPhotoHeader";
private const string LifeStampResourcePath = "UI_LifeStamp";
private const string PassportItemSlotResourcePath = "UI_PassportItemSlot";
private const string PassportLifeCircleFrameResourcePath = "UI_PassportHudLifeCircleFrame";
private const string PassportItemSlotResourcePath = "UI_PassportHudItemSlotFrame";
private const string PassportFlightMarkerResourcePath = "UI_PassportHudFlightMarker";
private const string PassportItemMarkerResourcePath = "UI_PassportHudItemMarker";
private const string TutorialGuideFrameResourcePath = "UI_TutorialAirportMonitorFrame";
private const string TutorialNextButtonResourcePath = "UI_TutorialNextButton";
private const string RightHudLuggageLabelKoreaResourcePath = "UI_RightHudLuggageLabel_Korea";
private const string RightHudLuggageLabelJapanResourcePath = "UI_RightHudLuggageLabel_Japan";
private const string RightHudLuggageLabelChinaResourcePath = "UI_RightHudLuggageLabel_China";
private const string RightHudLuggageLabelAmericaResourcePath = "UI_RightHudLuggageLabel_America";
private const int MaxSupplementPurchases = 4;
private const int MaxLifeWithSupplements = 5;
private const float PassportHudWidth = 261f;
private const float PassportHudHeight = 102f;
private const float PassportHudScale = 0.5f;
private const float RightInfoHudWidth = 260f;
private const float RightInfoHudHeight = 64f;
private const float RightInfoHudScale = 0.6f;
private const int PreparedItemSlotCount = 3;
private const float TutorialJumpGuideDuration = 3.5f;
private const float TutorialDoubleJumpGuideDuration = 3.5f;
private const float TutorialSlideGuideDuration = 3.5f;
@@ -115,9 +124,9 @@ public class GameManager : MonoBehaviour {
public float speedIncreaseRate = 0.05f;
public float maxGameSpeed = 2f;
public float hitSpeedPenalty = 0.3f;
private int shieldCharges = 0; // 방어막 아이템으로 막을 수 있는 남은 피격 횟수
private float itemInvincibleEndTime = 0f; // 라멘 무적 종료 시각
private float itemInvincibleEndTime = 0f; // 무적 아이템 종료 시각
private float mileageBonusEndTime = 0f; // 마일리지 카드 보너스 종료 시각
private int baseMaxLife;
public bool IsTutorialGuidePaused {
get { return tutorialGuidePauseActive || tutorialGuideDismissFrame == Time.frameCount; }
@@ -130,6 +139,7 @@ public class GameManager : MonoBehaviour {
{
// instance가 비어있다면(null) 그곳에 자기 자신을 할당
instance = this;
baseMaxLife = maxLife;
}
else
{
@@ -142,7 +152,21 @@ public class GameManager : MonoBehaviour {
}
}
private void ApplySupplementLifeBonus() {
if(baseMaxLife <= 0)
{
baseMaxLife = maxLife;
}
int supplementPurchases = GameFlowManager.Instance != null && GameFlowManager.Instance.SaveData != null
? Mathf.Clamp(GameFlowManager.Instance.SaveData.supplementPurchases, 0, MaxSupplementPurchases)
: 0;
int bonusLife = supplementPurchases / 2;
maxLife = Mathf.Min(MaxLifeWithSupplements, baseMaxLife + bonusLife);
}
private void Start() {
ApplySupplementLifeBonus();
currentLife = maxLife;
totalMileage = GameFlowManager.Instance != null
? GameFlowManager.Instance.SaveData.totalMileage
@@ -213,18 +237,21 @@ public class GameManager : MonoBehaviour {
public void BeginMap(MapDefinition mapDefinition) {
StopTutorialGuidePause(true);
Time.timeScale = 1f;
ApplySupplementLifeBonus();
currentMapDefinition = mapDefinition != null
? mapDefinition
: MapDatabase.GetMap(MapId.TutorialIncheon);
BackgroundLoop.ApplyResourceBackground(currentMapDefinition.backgroundResourcePath);
BackgroundLoop.ApplyResourceBackground(
currentMapDefinition.GetBackgroundResourcePaths(),
currentMapDefinition.backgroundFitScaleMultiplier,
currentMapDefinition.backgroundVerticalOffset);
UpdateRightInfoHudSkin();
score = 0;
stageMileage = 0;
currentLife = maxLife;
gameSpeed = minGameSpeed;
shieldCharges = 0;
itemInvincibleEndTime = 0f;
mileageBonusEndTime = 0f;
stageElapsedTime = 0f;
@@ -234,6 +261,7 @@ public class GameManager : MonoBehaviour {
isGameover = false;
tutorialGuideSequenceComplete = false;
currentTutorialGuideSequenceIndex = -1;
LoadPreparedDutyFreeItems();
SetTutorialGuide(TutorialGuideType.None, "", "");
RemoveActiveFinishGate();
@@ -249,6 +277,8 @@ public class GameManager : MonoBehaviour {
}
StartTutorialGuideSequence();
UpdateTutorialItemRowSprites();
UpdatePreparedItemSlotUI();
UpdateScoreUI();
}
@@ -370,20 +400,25 @@ public class GameManager : MonoBehaviour {
}
private bool ShouldUseTimeBasedTutorialGuide() {
return playTutorialGuideSequence && !HasCourseDrivenTutorialGuide();
return playTutorialGuideSequence
&& currentMapDefinition != null
&& currentMapDefinition.isTutorial
&& !HasCourseDrivenTutorialGuide();
}
private bool HasCourseDrivenTutorialGuide() {
if(currentMapDefinition == null
|| currentMapDefinition.openingSteps == null
|| currentMapDefinition.openingSteps.Length == 0)
CourseStep[] openingSteps = currentMapDefinition != null
? currentMapDefinition.GetOpeningSteps()
: null;
if(openingSteps == null || openingSteps.Length == 0)
{
return false;
}
for(int i = 0; i < currentMapDefinition.openingSteps.Length; i++)
for(int i = 0; i < openingSteps.Length; i++)
{
CourseStep step = currentMapDefinition.openingSteps[i];
CourseStep step = openingSteps[i];
if(step != null
&& (step.guideType != TutorialGuideType.None
|| !string.IsNullOrEmpty(step.title)
@@ -436,7 +471,14 @@ public class GameManager : MonoBehaviour {
SetTutorialGuide(TutorialGuideType.Slide, "슬라이드", "오른쪽 클릭을 누르고 있으면 낮게 지나갑니다.");
break;
case 3:
SetTutorialGuide(TutorialGuideType.Items, "아이템", "초밥 -> 라멘 -> 방어막 -> 마일리지 카드 -> 운동화 순서입니다.");
SetTutorialGuide(
TutorialGuideType.Items,
"아이템",
string.Format(
"{0} -> {1} -> {2} 순서입니다.",
GetGameplayItemDisplayName(TutorialItemType.HealthSushi),
GetGameplayItemDisplayName(TutorialItemType.InvincibleRamen),
GetGameplayItemDisplayName(TutorialItemType.EnergyDrink)));
break;
default:
tutorialGuideSequenceComplete = true;
@@ -452,6 +494,12 @@ public class GameManager : MonoBehaviour {
return;
}
if(!IsMileageBonusActive() && TryConsumePreparedDutyFreeItem(DutyFreeItemType.MileageCard))
{
mileageBonusEndTime = Time.time + mileageBonusDuration;
SpawnPreparedItemFeedback(DutyFreeItemType.MileageCard, false);
}
int finalAmount = IsMileageBonusActive()
? amount * Mathf.Max(1, mileageBonusMultiplier)
: amount;
@@ -483,6 +531,12 @@ public class GameManager : MonoBehaviour {
gameSpeed -= hitSpeedPenalty;
gameSpeed = Mathf.Max(gameSpeed, minGameSpeed);
if(currentLife < maxLife && TryConsumePreparedDutyFreeItem(DutyFreeItemType.Lunchbox))
{
currentLife = Mathf.Min(maxLife, currentLife + 1);
SpawnPreparedItemFeedback(DutyFreeItemType.Lunchbox, false);
}
UpdateScoreUI();
return currentLife <= 0;
@@ -501,35 +555,24 @@ public class GameManager : MonoBehaviour {
AddScore(3);
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "초밥", "방금 잃은 목숨을 1 회복합니다.");
string itemName = GetGameplayItemDisplayName(itemType);
SetTutorialGuide(TutorialGuideType.Items, itemName, itemName + "을 먹어 목숨을 1 회복합니다.");
}
break;
case TutorialItemType.InvincibleRamen:
itemInvincibleEndTime = Time.time + itemInvincibleDuration;
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "라멘", "짧은 시간 동안 다음 충돌 피해를 받지 않습니다.");
string itemName = GetGameplayItemDisplayName(itemType);
SetTutorialGuide(TutorialGuideType.Items, itemName, itemName + " 효과로 짧은 시간 동안 충돌 피해를 받지 않습니다.");
}
break;
case TutorialItemType.Shield:
shieldCharges = Mathf.Max(shieldCharges, 1);
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "방어막", "다음 충돌 1회를 방어막이 대신 막습니다.");
}
break;
case TutorialItemType.SpeedShoes:
case TutorialItemType.EnergyDrink:
gameSpeed = Mathf.Min(maxGameSpeed, gameSpeed + itemSpeedBoostAmount);
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "운동화", "속도를 즉시 회복해 늦어진 일정을 따라잡습니다.");
}
break;
case TutorialItemType.MileageCard:
mileageBonusEndTime = Time.time + mileageBonusDuration;
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "마일리지 카드", "잠시 동안 먹는 마일리지가 두 배로 들어옵니다.");
string itemName = GetGameplayItemDisplayName(itemType);
SetTutorialGuide(TutorialGuideType.Items, itemName, itemName + "로 속도를 조금 회복합니다.");
}
break;
}
@@ -547,25 +590,14 @@ public class GameManager : MonoBehaviour {
{
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "라멘", "무적 중이라 이번 충돌은 피해 없이 지나갑니다.");
string itemName = GetGameplayItemDisplayName(TutorialItemType.InvincibleRamen);
SetTutorialGuide(TutorialGuideType.Items, itemName, itemName + " 효과로 이번 충돌은 피해 없이 지나갑니다.");
}
SpawnItemBlockFeedback(TutorialItemType.InvincibleRamen);
return true;
}
if(shieldCharges > 0)
{
shieldCharges--;
SpawnItemBlockFeedback(TutorialItemType.Shield);
if(ShouldShowItemEffectGuide())
{
SetTutorialGuide(TutorialGuideType.Items, "방어막", "방어막이 충돌 피해를 한 번 막았습니다.");
}
UpdateScoreUI();
return true;
}
return false;
}
@@ -580,7 +612,21 @@ public class GameManager : MonoBehaviour {
}
private bool ShouldShowItemEffectGuide() {
return currentMapDefinition == null || !currentMapDefinition.isTutorial;
return currentMapDefinition != null && currentMapDefinition.isTutorial;
}
private Platform.PlatformSkin GetCurrentPlatformSkin() {
return currentMapDefinition != null
? currentMapDefinition.platformSkin
: Platform.PlatformSkin.Airport;
}
private string GetGameplayItemDisplayName(TutorialItemType itemType) {
return GameplayItemCatalog.GetDisplayName(itemType, GetCurrentPlatformSkin());
}
private string GetGameplayItemSpritePath(TutorialItemType itemType) {
return GameplayItemCatalog.GetSpriteAssetPath(itemType, GetCurrentPlatformSkin());
}
private bool IsItemInvincibleActive() {
@@ -843,34 +889,31 @@ public class GameManager : MonoBehaviour {
}
string itemText = (tutorialTitle + " " + tutorialMessage).Trim();
if(itemText.Contains("초밥"))
if(ContainsGameplayItemName(itemText, TutorialItemType.HealthSushi))
{
return 0;
}
if(itemText.Contains("라멘"))
if(ContainsGameplayItemName(itemText, TutorialItemType.InvincibleRamen))
{
return 1;
}
if(itemText.Contains("방어막"))
if(ContainsGameplayItemName(itemText, TutorialItemType.EnergyDrink))
{
return 2;
}
if(itemText.Contains("운동화"))
{
return 3;
}
if(itemText.Contains("마일리지"))
{
return 4;
}
return -1;
}
private bool ContainsGameplayItemName(string text, TutorialItemType itemType) {
return text.Contains(GameplayItemCatalog.GetDisplayName(itemType, Platform.PlatformSkin.Airport))
|| text.Contains(GameplayItemCatalog.GetDisplayName(itemType, Platform.PlatformSkin.Japan))
|| text.Contains(GameplayItemCatalog.GetDisplayName(itemType, Platform.PlatformSkin.China))
|| text.Contains(GameplayItemCatalog.GetDisplayName(itemType, Platform.PlatformSkin.USA));
}
private Color GetActiveLifeIconColor(Image lifeIcon) {
if(lifeIcon != null && lifeIcon.sprite == null)
{
@@ -990,15 +1033,54 @@ public class GameManager : MonoBehaviour {
hudBackground.color = hudBackground.sprite != null
? Color.white
: new Color(0.04f, 0.08f, 0.11f, 0.68f);
hudBackground.preserveAspect = true;
hudBackground.preserveAspect = false;
hudBackground.raycastTarget = false;
CreateFaceIcon(hudObject.transform);
CreatePassportHudMarkers(hudObject.transform);
CreateLifeIcons(hudObject.transform);
CreateItemSlots(hudObject.transform);
}
private void CreatePassportHudMarkers(Transform parent) {
CreatePassportHudImage(
parent,
"Flight Marker",
PassportFlightMarkerResourcePath,
new Vector2(132f, -12f),
new Vector2(118f, 17f),
Color.white,
false);
CreatePassportHudImage(
parent,
"Item Marker",
PassportItemMarkerResourcePath,
new Vector2(132f, -50f),
new Vector2(118f, 17f),
Color.white,
false);
}
private void CreateFaceIcon(Transform parent) {
CreatePassportHudImage(
parent,
"Photo Header",
PassportPhotoHeaderResourcePath,
new Vector2(45f, -8f),
new Vector2(72f, 14f),
Color.white,
false);
CreatePassportHudImage(
parent,
"Photo Frame",
PassportPhotoFrameResourcePath,
new Vector2(28f, -14f),
new Vector2(96f, 76f),
Color.white,
false);
GameObject faceFrame = new GameObject("Face Frame", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
faceFrame.transform.SetParent(parent, false);
@@ -1006,8 +1088,8 @@ public class GameManager : MonoBehaviour {
frameRect.anchorMin = new Vector2(0f, 1f);
frameRect.anchorMax = new Vector2(0f, 1f);
frameRect.pivot = new Vector2(0f, 1f);
frameRect.anchoredPosition = new Vector2(32f, -20f);
frameRect.sizeDelta = new Vector2(72f, 68f);
frameRect.anchoredPosition = new Vector2(33f, -22f);
frameRect.sizeDelta = new Vector2(86f, 64f);
Image frameImage = faceFrame.GetComponent<Image>();
frameImage.color = new Color(1f, 1f, 1f, 0f);
@@ -1032,12 +1114,22 @@ public class GameManager : MonoBehaviour {
}
private void CreateLifeIcons(Transform parent) {
Sprite frameSprite = LoadHudSprite(PassportLifeCircleFrameResourcePath);
Sprite lifeSprite = LoadHudSprite(LifeStampResourcePath);
int iconCount = Mathf.Max(maxLife, 1);
lifeIcons = new Image[iconCount];
for(int i = 0; i < iconCount; i++)
{
CreatePassportHudImage(
parent,
"Life Circle Frame " + (i + 1),
PassportLifeCircleFrameResourcePath,
new Vector2(134f + (i * 39f), -18f),
new Vector2(34f, 34f),
frameSprite != null ? Color.white : new Color(0.93f, 0.73f, 0.28f, 0.75f),
true);
GameObject lifeObject = new GameObject("Life Stamp " + (i + 1), typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
lifeObject.transform.SetParent(parent, false);
@@ -1045,8 +1137,8 @@ public class GameManager : MonoBehaviour {
lifeRect.anchorMin = new Vector2(0f, 1f);
lifeRect.anchorMax = new Vector2(0f, 1f);
lifeRect.pivot = new Vector2(0f, 1f);
lifeRect.anchoredPosition = new Vector2(125f + (i * 45f), -23f);
lifeRect.sizeDelta = new Vector2(28f, 28f);
lifeRect.anchoredPosition = new Vector2(139f + (i * 39f), -23f);
lifeRect.sizeDelta = new Vector2(24f, 24f);
Image lifeImage = lifeObject.GetComponent<Image>();
lifeImage.sprite = lifeSprite;
@@ -1061,8 +1153,9 @@ public class GameManager : MonoBehaviour {
private void CreateItemSlots(Transform parent) {
Sprite slotSprite = LoadHudSprite(PassportItemSlotResourcePath);
itemSlotIcons = new Image[PreparedItemSlotCount];
for(int i = 0; i < 3; i++)
for(int i = 0; i < PreparedItemSlotCount; i++)
{
GameObject slotObject = new GameObject("Item Slot " + (i + 1), typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
slotObject.transform.SetParent(parent, false);
@@ -1071,8 +1164,8 @@ public class GameManager : MonoBehaviour {
slotRect.anchorMin = new Vector2(0f, 1f);
slotRect.anchorMax = new Vector2(0f, 1f);
slotRect.pivot = new Vector2(0f, 1f);
slotRect.anchoredPosition = new Vector2(125f + (i * 45f), -62f);
slotRect.sizeDelta = new Vector2(30f, 30f);
slotRect.anchoredPosition = new Vector2(132f + (i * 40f), -65f);
slotRect.sizeDelta = new Vector2(36f, 24f);
Image slotImage = slotObject.GetComponent<Image>();
slotImage.sprite = slotSprite;
@@ -1081,9 +1174,189 @@ public class GameManager : MonoBehaviour {
: new Color(0.92f, 0.97f, 1f, 0.22f);
slotImage.preserveAspect = true;
slotImage.raycastTarget = false;
GameObject iconObject = new GameObject("Prepared Item Icon " + (i + 1), typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
iconObject.transform.SetParent(slotObject.transform, false);
RectTransform iconRect = iconObject.GetComponent<RectTransform>();
iconRect.anchorMin = Vector2.zero;
iconRect.anchorMax = Vector2.one;
iconRect.offsetMin = new Vector2(7f, 2f);
iconRect.offsetMax = new Vector2(-7f, -2f);
Image iconImage = iconObject.GetComponent<Image>();
iconImage.preserveAspect = true;
iconImage.raycastTarget = false;
iconImage.gameObject.SetActive(false);
itemSlotIcons[i] = iconImage;
}
}
private void LoadPreparedDutyFreeItems() {
if(preparedDutyFreeItems == null || preparedDutyFreeItems.Length != PreparedItemSlotCount)
{
preparedDutyFreeItems = new DutyFreeItemType[PreparedItemSlotCount];
}
for(int i = 0; i < preparedDutyFreeItems.Length; i++)
{
preparedDutyFreeItems[i] = DutyFreeItemType.None;
}
GameSaveData saveData = GameFlowManager.Instance != null
? GameFlowManager.Instance.SaveData
: null;
if(saveData == null)
{
return;
}
int slotCount = Mathf.Clamp(saveData.bagSlots, 1, PreparedItemSlotCount);
for(int i = 0; i < slotCount; i++)
{
preparedDutyFreeItems[i] = NormalizePreparedItem(GetPreparedItemFromSave(saveData, i));
}
}
private DutyFreeItemType GetPreparedItemFromSave(GameSaveData saveData, int slotIndex) {
switch(slotIndex)
{
case 0:
return saveData.equippedSlot1;
case 1:
return saveData.equippedSlot2;
case 2:
return saveData.equippedSlot3;
default:
return DutyFreeItemType.None;
}
}
private DutyFreeItemType NormalizePreparedItem(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
case DutyFreeItemType.MileageCard:
return itemType;
default:
return DutyFreeItemType.None;
}
}
private bool TryConsumePreparedDutyFreeItem(DutyFreeItemType itemType) {
if(preparedDutyFreeItems == null || itemType == DutyFreeItemType.None)
{
return false;
}
for(int i = 0; i < preparedDutyFreeItems.Length; i++)
{
if(preparedDutyFreeItems[i] != itemType)
{
continue;
}
preparedDutyFreeItems[i] = DutyFreeItemType.None;
if(GameFlowManager.Instance != null)
{
GameFlowManager.Instance.ConsumeEquippedDutyFreeItem(i, itemType);
}
UpdatePreparedItemSlotUI();
return true;
}
return false;
}
private void UpdatePreparedItemSlotUI() {
if(itemSlotIcons == null)
{
return;
}
for(int i = 0; i < itemSlotIcons.Length; i++)
{
Image iconImage = itemSlotIcons[i];
if(iconImage == null)
{
continue;
}
DutyFreeItemType itemType = preparedDutyFreeItems != null && i < preparedDutyFreeItems.Length
? preparedDutyFreeItems[i]
: DutyFreeItemType.None;
Sprite itemSprite = GetPreparedDutyFreeItemSprite(itemType);
iconImage.sprite = itemSprite;
iconImage.color = itemSprite != null ? Color.white : new Color(1f, 1f, 1f, 0f);
iconImage.gameObject.SetActive(itemSprite != null);
}
}
private Sprite GetPreparedDutyFreeItemSprite(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
return LoadTutorialSprite("Assets/Sprites/Items/Item_Lunchbox.png");
case DutyFreeItemType.MileageCard:
return LoadTutorialSprite("Assets/Sprites/Items/Item_Mileage_Card.png");
default:
return null;
}
}
private void SpawnPreparedItemFeedback(DutyFreeItemType itemType, bool blockedDamage) {
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
if(playerObject == null)
{
return;
}
ItemFeedbackEffect.Spawn(
playerObject.transform.position,
GetPreparedDutyFreeItemSprite(itemType),
GetPreparedItemFeedbackType(itemType),
blockedDamage);
}
private TutorialItemType GetPreparedItemFeedbackType(DutyFreeItemType itemType) {
switch(itemType)
{
case DutyFreeItemType.Lunchbox:
return TutorialItemType.HealthSushi;
case DutyFreeItemType.MileageCard:
return TutorialItemType.MileageCard;
default:
return TutorialItemType.HealthSushi;
}
}
private Image CreatePassportHudImage(
Transform parent,
string objectName,
string resourcePath,
Vector2 anchoredPosition,
Vector2 sizeDelta,
Color fallbackColor,
bool preserveAspect) {
GameObject imageObject = new GameObject(objectName, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
imageObject.transform.SetParent(parent, false);
RectTransform rectTransform = imageObject.GetComponent<RectTransform>();
rectTransform.anchorMin = new Vector2(0f, 1f);
rectTransform.anchorMax = new Vector2(0f, 1f);
rectTransform.pivot = new Vector2(0f, 1f);
rectTransform.anchoredPosition = anchoredPosition;
rectTransform.sizeDelta = sizeDelta;
Image image = imageObject.GetComponent<Image>();
image.sprite = LoadHudSprite(resourcePath);
image.color = image.sprite != null ? Color.white : fallbackColor;
image.preserveAspect = preserveAspect;
image.raycastTarget = false;
return image;
}
private Sprite LoadHudSprite(string resourcePath) {
Sprite sprite = Resources.Load<Sprite>(resourcePath);
if(sprite != null)
@@ -1189,11 +1462,9 @@ public class GameManager : MonoBehaviour {
private void CreateTutorialItemRow(Transform parent) {
string[] itemAssetPaths = {
"Assets/Sprites/Items/Item_Health_Sushi.png",
"Assets/Sprites/Items/Item_Invincible_Ramen.png",
"Assets/Sprites/Items/Item_Shield.png",
"Assets/Sprites/Items/Item_Speed_Shoes.png",
"Assets/Sprites/Items/Item_Mileage_Card.png"
GetGameplayItemSpritePath(TutorialItemType.HealthSushi),
GetGameplayItemSpritePath(TutorialItemType.InvincibleRamen),
GetGameplayItemSpritePath(TutorialItemType.EnergyDrink)
};
tutorialItemImages = new Image[itemAssetPaths.Length];
@@ -1219,6 +1490,27 @@ public class GameManager : MonoBehaviour {
}
}
private void UpdateTutorialItemRowSprites() {
if(tutorialItemImages == null || tutorialItemImages.Length < 3)
{
return;
}
TutorialItemType[] itemTypes = {
TutorialItemType.HealthSushi,
TutorialItemType.InvincibleRamen,
TutorialItemType.EnergyDrink
};
for(int i = 0; i < itemTypes.Length; i++)
{
if(tutorialItemImages[i] != null)
{
tutorialItemImages[i].sprite = LoadTutorialSprite(GetGameplayItemSpritePath(itemTypes[i]));
}
}
}
private void CreateTutorialNextButton(Transform parent) {
GameObject buttonObject = new GameObject("Tutorial Confirm Button", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
buttonObject.transform.SetParent(parent, false);
@@ -1519,9 +1811,9 @@ public class GameManager : MonoBehaviour {
ClearMarkers();
float cursor = 0f;
AddStepMarkers(mapDefinition.openingSteps, ref cursor, targetDistance);
AddStepMarkers(mapDefinition.GetOpeningSteps(), ref cursor, targetDistance);
CourseStep[] loopSteps = mapDefinition.loopSteps;
CourseStep[] loopSteps = mapDefinition.GetLoopSteps();
int loopGuard = 0;
while(cursor < targetDistance
&& loopSteps != null
@@ -1638,12 +1930,8 @@ public class GameManager : MonoBehaviour {
return "+";
case Platform.ItemPattern.InvincibleRamen:
return "R";
case Platform.ItemPattern.Shield:
return "G";
case Platform.ItemPattern.SpeedShoes:
case Platform.ItemPattern.EnergyDrink:
return "V";
case Platform.ItemPattern.MileageCard:
return "M";
default:
return "I";
}
+121
View File
@@ -0,0 +1,121 @@
public static class GameplayItemCatalog {
public static string GetDisplayName(GameManager.TutorialItemType itemType, Platform.PlatformSkin platformSkin) {
switch(itemType)
{
case GameManager.TutorialItemType.HealthSushi:
return GetHealthItemName(platformSkin);
case GameManager.TutorialItemType.InvincibleRamen:
return GetInvincibleItemName(platformSkin);
case GameManager.TutorialItemType.EnergyDrink:
return "에너지 드링크";
case GameManager.TutorialItemType.MileageCard:
return "마일리지 카드";
default:
return "";
}
}
public static string GetSpriteAssetPath(GameManager.TutorialItemType itemType, Platform.PlatformSkin platformSkin) {
switch(itemType)
{
case GameManager.TutorialItemType.HealthSushi:
return GetHealthItemAssetPath(platformSkin);
case GameManager.TutorialItemType.InvincibleRamen:
return GetInvincibleItemAssetPath(platformSkin);
case GameManager.TutorialItemType.EnergyDrink:
return GetEnergyDrinkAssetPath(platformSkin);
case GameManager.TutorialItemType.MileageCard:
return "Assets/Sprites/Items/Item_Mileage_Card.png";
default:
return "";
}
}
public static GameManager.TutorialItemType ToTutorialItemType(Platform.ItemPattern itemPattern) {
switch(itemPattern)
{
case Platform.ItemPattern.InvincibleRamen:
return GameManager.TutorialItemType.InvincibleRamen;
case Platform.ItemPattern.EnergyDrink:
return GameManager.TutorialItemType.EnergyDrink;
case Platform.ItemPattern.HealthSushi:
default:
return GameManager.TutorialItemType.HealthSushi;
}
}
private static string GetHealthItemName(Platform.PlatformSkin platformSkin) {
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "초밥";
case Platform.PlatformSkin.China:
return "만두";
case Platform.PlatformSkin.USA:
return "햄버거";
case Platform.PlatformSkin.Airport:
default:
return "김밥";
}
}
private static string GetInvincibleItemName(Platform.PlatformSkin platformSkin) {
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "라멘";
case Platform.PlatformSkin.China:
return "훠궈";
case Platform.PlatformSkin.USA:
return "핫도그";
case Platform.PlatformSkin.Airport:
default:
return "바나나우유";
}
}
private static string GetHealthItemAssetPath(Platform.PlatformSkin platformSkin) {
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "Assets/Sprites/RegionalItems/Regional_Japan_Sushi.png";
case Platform.PlatformSkin.China:
return "Assets/Sprites/RegionalItems/Regional_China_Dumpling.png";
case Platform.PlatformSkin.USA:
return "Assets/Sprites/RegionalItems/Regional_USA_Burger.png";
case Platform.PlatformSkin.Airport:
default:
return "Assets/Sprites/RegionalItems/Regional_Korea_Kimbap.png";
}
}
private static string GetInvincibleItemAssetPath(Platform.PlatformSkin platformSkin) {
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "Assets/Sprites/RegionalItems/Regional_Japan_Ramen.png";
case Platform.PlatformSkin.China:
return "Assets/Sprites/RegionalItems/Regional_China_Hotpot.png";
case Platform.PlatformSkin.USA:
return "Assets/Sprites/RegionalItems/Regional_USA_Hotdog.png";
case Platform.PlatformSkin.Airport:
default:
return "Assets/Sprites/RegionalItems/Regional_Korea_BananaMilk.png";
}
}
private static string GetEnergyDrinkAssetPath(Platform.PlatformSkin platformSkin) {
switch(platformSkin)
{
case Platform.PlatformSkin.Japan:
return "Assets/Sprites/RegionalItems/Regional_Japan_EnergyDrink.png";
case Platform.PlatformSkin.China:
return "Assets/Sprites/RegionalItems/Regional_China_EnergyDrink.png";
case Platform.PlatformSkin.USA:
return "Assets/Sprites/RegionalItems/Regional_USA_EnergyDrink.png";
case Platform.PlatformSkin.Airport:
default:
return "Assets/Sprites/RegionalItems/Regional_Korea_EnergyDrink.png";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ea4c33f88974b12862ce6f4a258f1f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+4 -10
View File
@@ -151,16 +151,14 @@ public class ItemFeedbackEffect : MonoBehaviour
{
if(blockedDamage)
{
return itemType == GameManager.TutorialItemType.InvincibleRamen ? "SAFE" : "BLOCK";
return "SAFE";
}
switch(itemType)
{
case GameManager.TutorialItemType.InvincibleRamen:
return "SAFE";
case GameManager.TutorialItemType.Shield:
return "SHIELD";
case GameManager.TutorialItemType.SpeedShoes:
case GameManager.TutorialItemType.EnergyDrink:
return "SPEED";
case GameManager.TutorialItemType.MileageCard:
return "x2";
@@ -174,18 +172,14 @@ public class ItemFeedbackEffect : MonoBehaviour
{
if(blockedDamage)
{
return itemType == GameManager.TutorialItemType.InvincibleRamen
? new Color(1f, 0.76f, 0.24f, 1f)
: new Color(0.36f, 0.82f, 1f, 1f);
return new Color(1f, 0.76f, 0.24f, 1f);
}
switch(itemType)
{
case GameManager.TutorialItemType.InvincibleRamen:
return new Color(1f, 0.58f, 0.12f, 1f);
case GameManager.TutorialItemType.Shield:
return new Color(0.32f, 0.78f, 1f, 1f);
case GameManager.TutorialItemType.SpeedShoes:
case GameManager.TutorialItemType.EnergyDrink:
return new Color(0.35f, 1f, 0.5f, 1f);
case GameManager.TutorialItemType.MileageCard:
return new Color(1f, 0.92f, 0.26f, 1f);
+1 -3
View File
@@ -62,9 +62,7 @@ public class ItemPickup : MonoBehaviour
{
case GameManager.TutorialItemType.InvincibleRamen:
return new Color(1f, 0.58f, 0.12f, 1f);
case GameManager.TutorialItemType.Shield:
return new Color(0.32f, 0.78f, 1f, 1f);
case GameManager.TutorialItemType.SpeedShoes:
case GameManager.TutorialItemType.EnergyDrink:
return new Color(0.35f, 1f, 0.5f, 1f);
case GameManager.TutorialItemType.MileageCard:
return new Color(1f, 0.92f, 0.26f, 1f);
+1 -20
View File
@@ -1,5 +1,3 @@
using static CourseStepFactory;
public static class AmericaMap {
public static MapDefinition Create() {
return new MapDefinition {
@@ -15,24 +13,7 @@ public static class AmericaMap {
clearMileageReward = 500,
hasNextMap = false,
nextMapId = MapId.America,
openingSteps = new CourseStep[] {
Basic(),
Basic()
},
loopSteps = new CourseStep[] {
Basic(),
Jump(Platform.PlatformPattern.LowMid),
Basic(GroundY, 0.86f),
Slide(SlideObstaclePattern.SlideLayout.WidePair),
Basic(),
Aerial(AerialY + 0.2f, 1.0f),
Basic(GroundY, 0.86f),
Jump(Platform.PlatformPattern.LowLeft),
Basic(),
Slide(SlideObstaclePattern.SlideLayout.CenterPair),
Basic(),
Jump(Platform.PlatformPattern.LowRight)
}
courseProfile = AmericaCourseProfile.Create()
};
}
}
+2 -19
View File
@@ -1,5 +1,3 @@
using static CourseStepFactory;
public static class ChinaMap {
public static MapDefinition Create() {
return new MapDefinition {
@@ -8,29 +6,14 @@ public static class ChinaMap {
routeName = "베이징 시장길",
isTutorial = false,
platformSkin = Platform.PlatformSkin.China,
backgroundResourcePath = "",
backgroundResourcePath = "Map_China_Beijing",
finishGateStyle = FinishGateStyle.ChinaPaifang,
targetDistance = 1450f,
timeLimit = 115f,
clearMileageReward = 350,
hasNextMap = true,
nextMapId = MapId.America,
openingSteps = new CourseStep[] {
Basic(),
Basic()
},
loopSteps = new CourseStep[] {
Basic(),
Jump(Platform.PlatformPattern.LowRight),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.LeftPair),
Basic(),
Aerial(AerialY + 0.15f, 1.08f),
Basic(GroundY, LandingSpacing),
Jump(),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.RightPair)
}
courseProfile = ChinaCourseProfile.Create()
};
}
}
+2 -18
View File
@@ -1,5 +1,3 @@
using static CourseStepFactory;
public static class JapanMap {
public static MapDefinition Create() {
return new MapDefinition {
@@ -8,28 +6,14 @@ public static class JapanMap {
routeName = "후쿠오카 라멘 투어",
isTutorial = false,
platformSkin = Platform.PlatformSkin.Japan,
backgroundResourcePath = "",
backgroundResourcePath = "Map_Japan_Fukuoka",
finishGateStyle = FinishGateStyle.JapanTorii,
targetDistance = 1200f,
timeLimit = 105f,
clearMileageReward = 250,
hasNextMap = true,
nextMapId = MapId.China,
openingSteps = new CourseStep[] {
Basic(),
Basic()
},
loopSteps = new CourseStep[] {
Basic(),
Jump(),
Basic(GroundY, LandingSpacing),
Aerial(),
Basic(GroundY, LandingSpacing),
Slide(SlideObstaclePattern.SlideLayout.CenterPair),
Basic(),
Jump(Platform.PlatformPattern.LowLeft),
Basic()
}
courseProfile = JapanCourseProfile.Create()
};
}
}
@@ -1,5 +1,3 @@
using static CourseStepFactory;
public static class TutorialIncheonMap {
public static MapDefinition Create() {
return new MapDefinition {
@@ -8,49 +6,23 @@ public static class TutorialIncheonMap {
routeName = "인천공항 출발 준비",
isTutorial = true,
platformSkin = Platform.PlatformSkin.Airport,
backgroundResourcePath = "Map_Tutorial_Incheon_Airport",
backgroundResourcePath = "StageBackgrounds/Incheon/Stage_Incheon_DarkLower_Segment_01_CheckInHall",
backgroundResourcePaths = new string[] {
"StageBackgrounds/Incheon/Stage_Incheon_DarkLower_Segment_01_CheckInHall",
"StageBackgrounds/Incheon/Stage_Incheon_DarkLower_Segment_02_DepartureSecurity",
"StageBackgrounds/Incheon/Stage_Incheon_DarkLower_Segment_03_DutyFreeConcourse",
"StageBackgrounds/Incheon/Stage_Incheon_DarkLower_Segment_04_MovingWalkwayWindows",
"StageBackgrounds/Incheon/Stage_Incheon_DarkLower_Segment_05_BoardingGate"
},
backgroundFitScaleMultiplier = 1f,
backgroundVerticalOffset = 0f,
finishGateStyle = FinishGateStyle.AirportDeparture,
targetDistance = 380f,
timeLimit = 85f,
clearMileageReward = 150,
hasNextMap = true,
nextMapId = MapId.Japan,
openingSteps = new CourseStep[] {
Basic(),
Basic(),
Basic(GroundY, BasicSpacing),
Jump(GameManager.TutorialGuideType.Jump, "점프 구간", "가방 장애물이 오면 왼쪽 클릭으로 뛰어넘습니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Aerial(GameManager.TutorialGuideType.DoubleJump, "2단 점프 구간", "다음 발판이 멀리 떨어져 있습니다.\n공중에서 왼쪽 클릭을 한 번 더 누릅니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Slide(GameManager.TutorialGuideType.Slide, "슬라이드 구간", "천장 장애물이 오면 오른쪽 클릭을 누르고 지나갑니다."),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "초밥", "잃은 목숨을 1 회복합니다.\n방금 줄어든 목숨을 채워 봅니다.", Platform.ItemPattern.HealthSushi),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "라멘", "잠깐 무적이 됩니다.\n먹은 뒤 다음 장애물에 일부러 부딪혀 봅니다.", Platform.ItemPattern.InvincibleRamen),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing),
Basic(GroundY, BasicSpacing),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "방어막", "다음 충돌 1회를 막습니다.\n먹은 뒤 바로 충돌을 막아 봅니다.", Platform.ItemPattern.Shield),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, LandingSpacing),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "마일리지 카드", "잠깐 마일리지가 2배가 됩니다.\n먹고 바로 마일리지를 모아 봅니다.", Platform.ItemPattern.MileageCard),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.SafeLine),
ForceHit(GameManager.TutorialGuideType.None, "", ""),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "운동화", "느려진 속도를 회복합니다.\n방금 떨어진 속도를 되돌려 봅니다.", Platform.ItemPattern.SpeedShoes),
Basic(GroundY, LandingSpacing, Platform.MileagePattern.None, GameManager.TutorialGuideType.Items, "실전 연습", "시간이 지나면 달리기 속도는 조금씩 빨라지고, 장애물에 맞으면 느려집니다.\n방금 먹은 신발 아이템으로 회복한 뒤 출국 게이트까지 달려보세요."),
Basic(GroundY, BasicSpacing, Platform.MileagePattern.SafeLine),
Basic(),
Basic(),
Basic(),
Basic(GroundY, LandingSpacing)
},
loopSteps = CreateEasyLoop()
courseProfile = TutorialCourseProfile.Create()
};
}
}
+8 -2
View File
@@ -73,14 +73,20 @@ public static class MapDatabase {
isTutorial = source.isTutorial,
platformSkin = source.platformSkin,
backgroundResourcePath = source.backgroundResourcePath,
backgroundResourcePaths = source.backgroundResourcePaths != null
? (string[]) source.backgroundResourcePaths.Clone()
: null,
backgroundFitScaleMultiplier = source.backgroundFitScaleMultiplier,
backgroundVerticalOffset = source.backgroundVerticalOffset,
finishGateStyle = source.finishGateStyle,
targetDistance = source.targetDistance,
timeLimit = source.timeLimit,
clearMileageReward = source.clearMileageReward,
hasNextMap = source.hasNextMap,
nextMapId = source.nextMapId,
openingSteps = CourseStepFactory.CloneSteps(source.openingSteps),
loopSteps = CourseStepFactory.CloneSteps(source.loopSteps)
courseProfile = CourseProfile.Create(source.GetOpeningSteps(), source.GetLoopSteps()),
openingSteps = source.GetOpeningSteps(),
loopSteps = source.GetLoopSteps()
};
}
}
+27
View File
@@ -8,12 +8,39 @@ public class MapDefinition {
public bool isTutorial;
public Platform.PlatformSkin platformSkin;
public string backgroundResourcePath;
public string[] backgroundResourcePaths;
public float backgroundFitScaleMultiplier = 1f;
public float backgroundVerticalOffset = 0f;
public FinishGateStyle finishGateStyle;
public float targetDistance;
public float timeLimit;
public int clearMileageReward;
public bool hasNextMap;
public MapId nextMapId;
public CourseProfile courseProfile;
public CourseStep[] openingSteps;
public CourseStep[] loopSteps;
public CourseStep[] GetOpeningSteps() {
return courseProfile != null
? courseProfile.GetOpeningSteps()
: CourseStepFactory.CloneSteps(openingSteps);
}
public CourseStep[] GetLoopSteps() {
return courseProfile != null
? courseProfile.GetLoopSteps()
: CourseStepFactory.CloneSteps(loopSteps);
}
public string[] GetBackgroundResourcePaths() {
if(backgroundResourcePaths != null && backgroundResourcePaths.Length > 0)
{
return (string[]) backgroundResourcePaths.Clone();
}
return string.IsNullOrEmpty(backgroundResourcePath)
? new string[0]
: new string[] { backgroundResourcePath };
}
}
+5 -47
View File
@@ -27,9 +27,7 @@ public class Platform : MonoBehaviour {
None,
HealthSushi,
InvincibleRamen,
Shield,
SpeedShoes,
MileageCard
EnergyDrink
}
public enum PlatformSkin {
@@ -224,30 +222,13 @@ public class Platform : MonoBehaviour {
return;
}
GameManager.TutorialItemType itemType = GetTutorialItemType(reservedItemPattern);
GameManager.TutorialItemType itemType = GameplayItemCatalog.ToTutorialItemType(reservedItemPattern);
ItemPickup pickup = itemPickups[0];
pickup.Setup(LoadItemSprite(itemType), itemType);
pickup.transform.localPosition = GetParentScaleCompensatedPosition(GetItemPosition(reservedItemPattern));
pickup.ResetPickup();
}
private GameManager.TutorialItemType GetTutorialItemType(ItemPattern itemPattern) {
switch(itemPattern)
{
case ItemPattern.InvincibleRamen:
return GameManager.TutorialItemType.InvincibleRamen;
case ItemPattern.Shield:
return GameManager.TutorialItemType.Shield;
case ItemPattern.SpeedShoes:
return GameManager.TutorialItemType.SpeedShoes;
case ItemPattern.MileageCard:
return GameManager.TutorialItemType.MileageCard;
case ItemPattern.HealthSushi:
default:
return GameManager.TutorialItemType.HealthSushi;
}
}
private Vector2 GetItemPosition(ItemPattern itemPattern) {
const float frontX = -1.45f;
@@ -257,11 +238,7 @@ public class Platform : MonoBehaviour {
return new Vector2(frontX, 1.55f);
case ItemPattern.InvincibleRamen:
return new Vector2(frontX, 1.55f);
case ItemPattern.Shield:
return new Vector2(frontX, 1.55f);
case ItemPattern.SpeedShoes:
return new Vector2(frontX, 1.55f);
case ItemPattern.MileageCard:
case ItemPattern.EnergyDrink:
return new Vector2(frontX, 1.55f);
default:
return new Vector2(0f, 1.55f);
@@ -328,31 +305,12 @@ public class Platform : MonoBehaviour {
return "Assets/Sprites/Mileage/Mileage_Korea.png";
}
}
#endif
private Sprite LoadItemSprite(GameManager.TutorialItemType itemType) {
#if UNITY_EDITOR
string assetPath = "";
switch(itemType)
{
case GameManager.TutorialItemType.InvincibleRamen:
assetPath = "Assets/Sprites/Items/Item_Invincible_Ramen.png";
break;
case GameManager.TutorialItemType.Shield:
assetPath = "Assets/Sprites/Items/Item_Shield.png";
break;
case GameManager.TutorialItemType.SpeedShoes:
assetPath = "Assets/Sprites/Items/Item_Speed_Shoes.png";
break;
case GameManager.TutorialItemType.MileageCard:
assetPath = "Assets/Sprites/Items/Item_Mileage_Card.png";
break;
case GameManager.TutorialItemType.HealthSushi:
default:
assetPath = "Assets/Sprites/Items/Item_Health_Sushi.png";
break;
}
string assetPath = GameplayItemCatalog.GetSpriteAssetPath(itemType, reservedSkin);
return AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
#else
+9 -152
View File
@@ -29,11 +29,7 @@ public class PlatformSpawner : MonoBehaviour {
public float tutorialSpawnInterval = 2.2f; // 튜토리얼 발판 배치 간격
private float timeBetSpawn; // 다음 배치까지의 시간 간격
private const float FallbackGroundY = -2.4f;
private const float FallbackBasicSpacing = 0.58f;
private const float FallbackActionSpacing = 0.72f;
private const float FallbackLandingSpacing = 0.64f;
private const float FallbackAerialSpacing = 2.05f;
public float yMin = -3.5f; // 배치할 위치의 최소 y값
public float yMax = 1.5f; // 배치할 위치의 최대 y값
@@ -64,8 +60,8 @@ public class PlatformSpawner : MonoBehaviour {
usePatternCourse = true;
tutorialMode = mapDefinition.isTutorial;
mapPlatformSkin = mapDefinition.platformSkin;
tutorialSteps = CourseStepFactory.CloneSteps(mapDefinition.openingSteps);
stagePatternSteps = CourseStepFactory.CloneSteps(mapDefinition.loopSteps);
tutorialSteps = mapDefinition.GetOpeningSteps();
stagePatternSteps = mapDefinition.GetLoopSteps();
courseStepIndex = 0;
courseSpawningStopped = false;
pendingTutorialGuides.Clear();
@@ -479,63 +475,19 @@ public class PlatformSpawner : MonoBehaviour {
}
private void EnsurePatternSteps() {
EnsureTutorialSteps();
EnsureStagePatternSteps();
}
CourseProfile fallbackProfile = tutorialMode
? FallbackCourseProfile.CreateTutorialFallback()
: FallbackCourseProfile.CreateGeneralFallback();
private void EnsureTutorialSteps() {
if(tutorialSteps != null && tutorialSteps.Length > 0)
if(tutorialSteps == null || tutorialSteps.Length == 0)
{
return;
tutorialSteps = fallbackProfile.GetOpeningSteps();
}
tutorialSteps = new CourseStep[] {
CreateTutorialStep(CourseStepKind.Jump, Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing),
CreateTutorialStep(CourseStepKind.DoubleJumpTakeoff, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackAerialSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing),
CreateTutorialStep(CourseStepKind.Slide, Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing),
CreateTutorialStep(CourseStepKind.ForceHit, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(CourseStepKind.HealthItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing, Platform.ItemPattern.HealthSushi),
CreateTutorialStep(CourseStepKind.InvincibleItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.InvincibleRamen),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackLandingSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing),
CreateTutorialStep(CourseStepKind.ShieldItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.Shield),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(CourseStepKind.MileageCardItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.MileageCard),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.ForceHit, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.WidePair, FallbackGroundY, FallbackActionSpacing),
CreateTutorialStep(CourseStepKind.SpeedItem, Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing, Platform.ItemPattern.SpeedShoes),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing),
CreateTutorialStep(CourseStepKind.None, Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, FallbackGroundY, FallbackBasicSpacing),
};
}
private void EnsureStagePatternSteps() {
if(stagePatternSteps != null && stagePatternSteps.Length > 0)
if(stagePatternSteps == null || stagePatternSteps.Length == 0)
{
return;
stagePatternSteps = fallbackProfile.GetLoopSteps();
}
stagePatternSteps = new CourseStep[] {
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackLandingSpacing, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.WidePair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.LowLeft, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.LeftPair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.LowRight, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.None, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackAerialSpacing, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.RightPair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
CreateStep(Platform.PlatformPattern.LowMid, Platform.MileagePattern.JumpArc, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Jump),
CreateStep(Platform.PlatformPattern.Empty, Platform.MileagePattern.SafeLine, SlideObstaclePattern.SlideLayout.Random, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackBasicSpacing, "", ""),
CreateStep(Platform.PlatformPattern.Slide, Platform.MileagePattern.SlideLine, SlideObstaclePattern.SlideLayout.CenterPair, GameManager.TutorialGuideType.None, FallbackGroundY, FallbackActionSpacing, "", "", Platform.ItemPattern.None, CoursePlatformType.Slide),
};
}
private CourseStep CreateStep(
@@ -563,99 +515,4 @@ public class PlatformSpawner : MonoBehaviour {
return step;
}
private CourseStep CreateTutorialStep(
CourseStepKind stepKind,
Platform.PlatformPattern platformPattern,
Platform.MileagePattern mileagePattern,
SlideObstaclePattern.SlideLayout slideLayout,
float yPosition,
float spawnInterval,
Platform.ItemPattern itemPattern = Platform.ItemPattern.None) {
GameManager.TutorialGuideType guideType = GameManager.TutorialGuideType.None;
string title = "";
string message = "";
switch(stepKind)
{
case CourseStepKind.Jump:
guideType = GameManager.TutorialGuideType.Jump;
title = "점프";
message = "가방 장애물이 보이면 왼쪽 클릭으로 뛰어넘습니다.";
break;
case CourseStepKind.DoubleJumpTakeoff:
guideType = GameManager.TutorialGuideType.DoubleJump;
title = "2단 점프";
message = "다음 발판이 멀리 있으면 공중에서 한 번 더 왼쪽 클릭합니다.";
break;
case CourseStepKind.Slide:
guideType = GameManager.TutorialGuideType.Slide;
title = "슬라이드";
message = "천장 표지판이 보이면 오른쪽 클릭을 누르고 지나갑니다.";
break;
case CourseStepKind.ForceHit:
guideType = GameManager.TutorialGuideType.Damage;
title = "피격";
message = "이번 구간은 일부러 맞아 목숨과 속도 감소를 확인합니다.";
break;
case CourseStepKind.HealthItem:
guideType = GameManager.TutorialGuideType.Items;
title = "초밥";
message = "초밥을 먹으면 잃은 목숨을 1 회복합니다.";
break;
case CourseStepKind.InvincibleItem:
guideType = GameManager.TutorialGuideType.Items;
title = "라멘";
message = "라멘을 먹고 다음 충돌을 피해 없이 지나가 봅니다.";
break;
case CourseStepKind.ShieldItem:
guideType = GameManager.TutorialGuideType.Items;
title = "방어막";
message = "방어막을 먹고 다음 충돌 1회를 막아 봅니다.";
break;
case CourseStepKind.SpeedItem:
guideType = GameManager.TutorialGuideType.Items;
title = "운동화";
message = "운동화는 속도를 회복해 일정이 늦어지는 것을 줄입니다.";
break;
case CourseStepKind.MileageCardItem:
guideType = GameManager.TutorialGuideType.Items;
title = "마일리지 카드";
message = "카드를 먹은 뒤 마일리지를 모으면 획득량이 늘어납니다.";
break;
}
return CreateStep(
platformPattern,
mileagePattern,
slideLayout,
guideType,
yPosition,
spawnInterval,
title,
message,
itemPattern,
GetCoursePlatformType(stepKind, platformPattern, yPosition));
}
private CoursePlatformType GetCoursePlatformType(CourseStepKind stepKind, Platform.PlatformPattern platformPattern, float yPosition) {
if(stepKind == CourseStepKind.ForceHit || platformPattern == Platform.PlatformPattern.ForceHit)
{
return CoursePlatformType.ForceHit;
}
if(stepKind == CourseStepKind.Slide || platformPattern == Platform.PlatformPattern.Slide)
{
return CoursePlatformType.Slide;
}
if(stepKind == CourseStepKind.Jump
|| platformPattern == Platform.PlatformPattern.LowLeft
|| platformPattern == Platform.PlatformPattern.LowMid
|| platformPattern == Platform.PlatformPattern.LowRight)
{
return CoursePlatformType.Jump;
}
return CoursePlatformType.Basic;
}
}
+55 -1
View File
@@ -5,6 +5,11 @@ using UnityEngine;
// PlayerController는 플레이어 캐릭터로서 Player 게임 오브젝트를 제어한다.
public class PlayerController : MonoBehaviour {
public AudioClip deathClip; // 사망시 재생할 오디오 클립
public AudioClip jumpClip; // 점프시 재생할 오디오 클립
public AudioClip doubleJumpClip; // 2단 점프시 재생할 오디오 클립
public AudioClip slideClip; // 슬라이딩 시작시 재생할 오디오 클립
[Range(0f, 1f)]
public float movementSfxVolume = 0.72f; // 이동 효과음 볼륨
public float jumpForce = 600f; // 점프 힘
public int maxJumpCount = 2; // 최대 점프 횟수
public float invincibleDuration = 1.5f; // 피해를 입은 뒤 무적 시간
@@ -37,6 +42,8 @@ public class PlayerController : MonoBehaviour {
animator = GetComponent<Animator>();
playerAudio = GetComponent<AudioSource>();
spriteRenderer = GetComponent<SpriteRenderer>();
ConfigureAudioSource();
LoadMovementSfxDefaults();
// 게임 콜라이더를 가져와서 현 상태를 저장
playerCollider = GetComponent<CapsuleCollider2D>();
@@ -64,7 +71,7 @@ public class PlayerController : MonoBehaviour {
// 리지드바디에 위쪽으로 힘을 주기
playerRigidbody.AddForce(new Vector2(0, jumpForce));
playerAudio.Play();
PlayJumpSfx();
}
else if(Input.GetMouseButtonUp(0) && playerRigidbody.linearVelocityY > 0)
{
@@ -174,6 +181,7 @@ public class PlayerController : MonoBehaviour {
private void StartSlide() {
isSliding = true;
animator.SetBool("Sliding", true);
PlayMovementSfx(slideClip, movementSfxVolume * 0.72f);
playerCollider.size = slidingColliderSize;
playerCollider.offset = slidingColliderOffset;
@@ -238,4 +246,50 @@ public class PlayerController : MonoBehaviour {
spriteRenderer.color = Color.white;
isInvincible = false;
}
private void ConfigureAudioSource() {
if(playerAudio == null)
{
playerAudio = gameObject.AddComponent<AudioSource>();
}
playerAudio.playOnAwake = false;
playerAudio.spatialBlend = 0f;
}
private void LoadMovementSfxDefaults() {
if(jumpClip == null)
{
jumpClip = Resources.Load<AudioClip>("SFX/SFX_Player_Jump");
}
if(doubleJumpClip == null)
{
doubleJumpClip = Resources.Load<AudioClip>("SFX/SFX_Player_DoubleJump");
}
if(slideClip == null)
{
slideClip = Resources.Load<AudioClip>("SFX/SFX_Player_Slide");
}
}
private void PlayJumpSfx() {
AudioClip clip = jumpCount > 1 && doubleJumpClip != null
? doubleJumpClip
: jumpClip;
float volume = jumpCount > 1
? movementSfxVolume * 0.9f
: movementSfxVolume;
PlayMovementSfx(clip, volume);
}
private void PlayMovementSfx(AudioClip clip, float volume) {
if(playerAudio == null || clip == null)
{
return;
}
playerAudio.PlayOneShot(clip, Mathf.Clamp01(volume));
}
}