Tune tutorial coin arcs and finish flow

This commit is contained in:
jongjae0305
2026-07-06 17:34:13 +09:00
parent 2ba3882f1b
commit 5423382296
17 changed files with 266 additions and 65 deletions

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+67 -21
View File
@@ -1,12 +1,15 @@
using TMPro;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public enum FinishGateStyle {
None,
AirportDeparture,
JapanTorii,
ChinaPaifang,
AmericaRoadSign
AmericaLiberty
}
public class FinishGate : MonoBehaviour {
@@ -75,8 +78,8 @@ public class FinishGate : MonoBehaviour {
case FinishGateStyle.ChinaPaifang:
BuildChinaPaifang();
break;
case FinishGateStyle.AmericaRoadSign:
BuildAmericaRoadSign();
case FinishGateStyle.AmericaLiberty:
BuildAmericaLibertyFallback();
break;
case FinishGateStyle.AirportDeparture:
default:
@@ -98,19 +101,7 @@ public class FinishGate : MonoBehaviour {
return false;
}
Sprite sprite = Resources.Load<Sprite>(resourcePath);
if(sprite == null)
{
Texture2D texture = Resources.Load<Texture2D>(resourcePath);
if(texture != null)
{
sprite = Sprite.Create(
texture,
new Rect(0f, 0f, texture.width, texture.height),
new Vector2(0.5f, 0.5f),
RuntimeSpritePixelsPerUnit);
}
}
Sprite sprite = LoadGateSprite(resourcePath);
if(sprite == null)
{
@@ -129,10 +120,55 @@ public class FinishGate : MonoBehaviour {
float spriteHeight = Mathf.Max(0.01f, sprite.bounds.size.y);
float scale = targetHeight / spriteHeight;
spriteObject.transform.localScale = new Vector3(scale, scale, 1f);
spriteObject.transform.localPosition = new Vector3(0f, targetHeight * 0.5f, 0f);
spriteObject.transform.localPosition = new Vector3(0f, targetHeight * 0.5f - GetSpriteGateBottomInset(style), 0f);
return true;
}
private Sprite LoadGateSprite(string resourcePath) {
Sprite sprite = Resources.Load<Sprite>(resourcePath);
if(sprite != null)
{
return sprite;
}
Sprite[] sprites = Resources.LoadAll<Sprite>(resourcePath);
if(sprites != null && sprites.Length > 0 && sprites[0] != null)
{
return sprites[0];
}
Texture2D texture = Resources.Load<Texture2D>(resourcePath);
if(texture != null)
{
return CreateRuntimeSprite(texture);
}
#if UNITY_EDITOR
string assetPath = "Assets/Resources/" + resourcePath + ".png";
sprite = AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
if(sprite != null)
{
return sprite;
}
texture = AssetDatabase.LoadAssetAtPath<Texture2D>(assetPath);
if(texture != null)
{
return CreateRuntimeSprite(texture);
}
#endif
return null;
}
private Sprite CreateRuntimeSprite(Texture2D texture) {
return Sprite.Create(
texture,
new Rect(0f, 0f, texture.width, texture.height),
new Vector2(0.5f, 0.5f),
RuntimeSpritePixelsPerUnit);
}
private string GetSpriteResourcePath(FinishGateStyle style) {
switch(style)
{
@@ -142,8 +178,8 @@ public class FinishGate : MonoBehaviour {
return "FinishGate_Japan_Torii";
case FinishGateStyle.ChinaPaifang:
return "FinishGate_China_Paifang";
case FinishGateStyle.AmericaRoadSign:
return "FinishGate_America_Highway";
case FinishGateStyle.AmericaLiberty:
return "FinishGate_America_Liberty";
default:
return "";
}
@@ -154,13 +190,23 @@ public class FinishGate : MonoBehaviour {
{
case FinishGateStyle.ChinaPaifang:
return 4.65f;
case FinishGateStyle.AmericaRoadSign:
case FinishGateStyle.AmericaLiberty:
return 4.45f;
default:
return 4.35f;
}
}
private float GetSpriteGateBottomInset(FinishGateStyle style) {
switch(style)
{
case FinishGateStyle.JapanTorii:
return 0.36f;
default:
return 0f;
}
}
private void BuildAirportDepartureGate() {
Color navy = new Color(0.05f, 0.2f, 0.34f, 1f);
Color blue = new Color(0.08f, 0.42f, 0.67f, 1f);
@@ -212,7 +258,7 @@ public class FinishGate : MonoBehaviour {
CreateBlock("Paifang Lantern Right", new Vector2(0.78f, 2.18f), new Vector2(0.26f, 0.34f), red, 7);
}
private void BuildAmericaRoadSign() {
private void BuildAmericaLibertyFallback() {
Color metal = new Color(0.48f, 0.56f, 0.58f, 1f);
Color green = new Color(0.03f, 0.38f, 0.22f, 1f);
Color yellow = new Color(1f, 0.8f, 0.18f, 1f);
+86 -16
View File
@@ -62,6 +62,7 @@ public class GameManager : MonoBehaviour {
private int tutorialGuideVersion = 0; // 오래된 자동 숨김 요청을 무시하기 위한 버전
private FinishGate activeFinishGate; // 목표 지점에 나타나는 맵별 클리어 게이트
private bool finishGateSpawned = false; // 현재 맵에서 도착 게이트를 이미 만들었는지 여부
private PlatformSpawner activePlatformSpawner; // 현재 맵의 발판/장애물 생성기
public int maxLife = 3; // 시작 목숨
public int initialMileage = 500; // 첫 시작시 지급할 초기 마일리지
@@ -222,10 +223,10 @@ public class GameManager : MonoBehaviour {
gameoverUI.SetActive(false);
}
PlatformSpawner platformSpawner = FindFirstObjectByType<PlatformSpawner>();
if(platformSpawner != null)
activePlatformSpawner = FindFirstObjectByType<PlatformSpawner>();
if(activePlatformSpawner != null)
{
platformSpawner.ApplyMap(currentMapDefinition);
activePlatformSpawner.ApplyMap(currentMapDefinition);
}
StartTutorialGuideSequence();
@@ -646,11 +647,31 @@ public class GameManager : MonoBehaviour {
return;
}
float gateGroundY = finishGateGroundY;
EnsureFinishGateLanding(gateGroundY);
GameObject gateObject = new GameObject("Finish Gate - " + currentMapDefinition.displayName);
gateObject.transform.position = new Vector3(finishGateSpawnX, finishGateGroundY, 0f);
gateObject.transform.position = new Vector3(finishGateSpawnX, gateGroundY, 0f);
activeFinishGate = gateObject.AddComponent<FinishGate>();
activeFinishGate.Setup(currentMapDefinition.finishGateStyle);
finishGateSpawned = true;
if(activePlatformSpawner != null)
{
activePlatformSpawner.StopCourseSpawningAfterFinishGate();
}
}
private void EnsureFinishGateLanding(float gateGroundY) {
if(activePlatformSpawner == null)
{
activePlatformSpawner = FindFirstObjectByType<PlatformSpawner>();
}
if(activePlatformSpawner != null)
{
activePlatformSpawner.SpawnFinishLandingPlatform(finishGateSpawnX, gateGroundY);
}
}
private bool ShouldUseFinishGate() {
@@ -665,6 +686,11 @@ public class GameManager : MonoBehaviour {
Destroy(activeFinishGate.gameObject);
activeFinishGate = null;
}
if(activePlatformSpawner != null)
{
activePlatformSpawner.ClearFinishLandingPlatform();
}
}
private void SyncTotalMileageToFlow() {
@@ -739,14 +765,15 @@ public class GameManager : MonoBehaviour {
tutorialBodyText.text = tutorialMessage;
}
bool showItemRow = tutorialGuideType == TutorialGuideType.Items;
Sprite mainSprite = GetTutorialMainSprite(tutorialGuideType);
bool showFullText = !showItemRow && mainSprite == null;
UpdateTutorialTextLayout(showItemRow, showFullText);
int activeItemImageIndex = GetTutorialItemImageIndex();
bool showItemImage = tutorialGuideType == TutorialGuideType.Items && HasTutorialItemImage(activeItemImageIndex);
Sprite mainSprite = showItemImage ? null : GetTutorialMainSprite(tutorialGuideType);
bool showFullText = !showItemImage && mainSprite == null;
UpdateTutorialTextLayout(showItemImage, showFullText);
if(tutorialMainImage != null)
{
tutorialMainImage.gameObject.SetActive(!showItemRow && mainSprite != null);
tutorialMainImage.gameObject.SetActive(!showItemImage && mainSprite != null);
tutorialMainImage.sprite = mainSprite;
}
@@ -761,7 +788,7 @@ public class GameManager : MonoBehaviour {
{
if(tutorialItemImages[i] != null)
{
tutorialItemImages[i].gameObject.SetActive(showItemRow && tutorialItemImages[i].sprite != null);
tutorialItemImages[i].gameObject.SetActive(showItemImage && i == activeItemImageIndex);
}
}
}
@@ -775,10 +802,10 @@ public class GameManager : MonoBehaviour {
if(showItemRow)
{
textPosition = new Vector2(154f, -8f);
textSize = new Vector2(178f, 17f);
bodyPosition = new Vector2(154f, -26f);
bodySize = new Vector2(178f, 31f);
textPosition = new Vector2(80f, -8f);
textSize = new Vector2(252f, 17f);
bodyPosition = new Vector2(80f, -26f);
bodySize = new Vector2(252f, 31f);
}
else if(showFullText)
{
@@ -801,6 +828,49 @@ public class GameManager : MonoBehaviour {
}
}
private bool HasTutorialItemImage(int itemImageIndex) {
return tutorialItemImages != null
&& itemImageIndex >= 0
&& itemImageIndex < tutorialItemImages.Length
&& tutorialItemImages[itemImageIndex] != null
&& tutorialItemImages[itemImageIndex].sprite != null;
}
private int GetTutorialItemImageIndex() {
if(tutorialGuideType != TutorialGuideType.Items)
{
return -1;
}
string itemText = (tutorialTitle + " " + tutorialMessage).Trim();
if(itemText.Contains("초밥"))
{
return 0;
}
if(itemText.Contains("라멘"))
{
return 1;
}
if(itemText.Contains("방어막"))
{
return 2;
}
if(itemText.Contains("운동화"))
{
return 3;
}
if(itemText.Contains("마일리지"))
{
return 4;
}
return -1;
}
private Color GetActiveLifeIconColor(Image lifeIcon) {
if(lifeIcon != null && lifeIcon.sprite == null)
{
@@ -1061,8 +1131,8 @@ public class GameManager : MonoBehaviour {
itemRect.anchorMin = new Vector2(0f, 0.5f);
itemRect.anchorMax = new Vector2(0f, 0.5f);
itemRect.pivot = new Vector2(0f, 0.5f);
itemRect.anchoredPosition = new Vector2(11f + (i * 27f), 0f);
itemRect.sizeDelta = new Vector2(24f, 24f);
itemRect.anchoredPosition = new Vector2(14f, 0f);
itemRect.sizeDelta = new Vector2(54f, 54f);
Image itemImage = itemObject.GetComponent<Image>();
itemImage.sprite = LoadTutorialSprite(itemAssetPaths[i]);
+1 -1
View File
@@ -193,7 +193,7 @@ public static class MapDatabase {
isTutorial = false,
platformSkin = Platform.PlatformSkin.USA,
backgroundResourcePath = "Map_America_NewYork",
finishGateStyle = FinishGateStyle.AmericaRoadSign,
finishGateStyle = FinishGateStyle.AmericaLiberty,
targetDistance = 1700f,
timeLimit = 125f,
clearMileageReward = 500,
+21 -5
View File
@@ -60,6 +60,8 @@ public class Platform : MonoBehaviour {
private const float MileageColliderRadius = 0.4f;
private const float ItemTokenScale = 0.68f;
private const float ItemColliderRadius = 0.46f;
private const float JumpMileageMinWidthScale = 1.8f;
private const float JumpMileageMaxWidthScale = 2.5f;
public void ConfigureForTutorial(
PlatformPattern pattern,
@@ -619,15 +621,29 @@ public class Platform : MonoBehaviour {
}
private void PlaceJumpMileageArc(float obstacleX) {
float widthScale = GetJumpMileageArcWidthScale();
Vector2[] positions = {
new Vector2(obstacleX - 1.2f, 1.65f),
new Vector2(obstacleX, 2.25f),
new Vector2(obstacleX + 1.2f, 1.65f),
new Vector2(obstacleX + 1.9f, 1.35f)
new Vector2(obstacleX - 2.25f * widthScale, 1.88f),
new Vector2(obstacleX - 1.15f * widthScale, 2.88f),
new Vector2(obstacleX, 3.58f),
new Vector2(obstacleX + 1.15f * widthScale, 2.88f),
new Vector2(obstacleX + 2.25f * widthScale, 1.88f)
};
PlaceMileageTokens(positions, smallMileageValue);
SetPickupValue(1, bonusMileageValue);
SetPickupValue(2, bonusMileageValue);
}
private float GetJumpMileageArcWidthScale() {
if(GameManager.instance == null)
{
return JumpMileageMinWidthScale;
}
float minSpeed = Mathf.Max(0.1f, GameManager.instance.minGameSpeed);
float maxSpeed = Mathf.Max(minSpeed + 0.1f, GameManager.instance.maxGameSpeed);
float speedProgress = Mathf.InverseLerp(minSpeed, maxSpeed, GameManager.instance.gameSpeed);
return Mathf.Lerp(JumpMileageMinWidthScale, JumpMileageMaxWidthScale, speedProgress);
}
private void PlaceSlideMileageLine() {
+49
View File
@@ -84,6 +84,8 @@ public class PlatformSpawner : MonoBehaviour {
private int courseStepIndex = 0; // 진행 중인 패턴 코스 순번
private Platform.PlatformSkin mapPlatformSkin = Platform.PlatformSkin.Airport; // 현재 맵에서 사용할 발판 스킨
private readonly List<PendingTutorialGuide> pendingTutorialGuides = new List<PendingTutorialGuide>(); // 실제 체험 직전에 띄울 설명들
private GameObject activeFinishLandingPlatform; // 도착 게이트가 공중에 뜨지 않도록 깔아두는 전용 발판
private bool courseSpawningStopped = false; // 도착 게이트 뒤로 새 발판/장애물이 나오지 않게 막는다.
private Vector2 poolPosition = new Vector2(0, -25); // 초반에 생성된 발판들을 화면 밖에 숨겨둘 위치
private float lastSpawnTime; // 마지막 배치 시점
@@ -100,7 +102,9 @@ public class PlatformSpawner : MonoBehaviour {
tutorialSteps = MapDatabase.CloneSteps(mapDefinition.openingSteps);
stagePatternSteps = MapDatabase.CloneSteps(mapDefinition.loopSteps);
courseStepIndex = 0;
courseSpawningStopped = false;
pendingTutorialGuides.Clear();
ClearFinishLandingPlatform();
ResetPoolIndices();
lastSpawnTime = Time.time;
timeBetSpawn = 0f;
@@ -109,6 +113,46 @@ public class PlatformSpawner : MonoBehaviour {
DeactivateAllPlatformPools();
}
public void SpawnFinishLandingPlatform(float spawnX, float groundY) {
if(platformPrefab == null)
{
return;
}
ClearFinishLandingPlatform();
activeFinishLandingPlatform = Instantiate(platformPrefab, poolPosition, Quaternion.identity);
activeFinishLandingPlatform.SetActive(false);
TutorialStep landingStep = CreateStep(
Platform.PlatformPattern.Empty,
Platform.MileagePattern.None,
SlideObstaclePattern.SlideLayout.Random,
GameManager.TutorialGuideType.None,
Mathf.Clamp(groundY, yMin, yMax),
FallbackLandingSpacing,
"",
"",
Platform.ItemPattern.None,
CoursePlatformType.Basic);
ConfigurePlatform(activeFinishLandingPlatform, landingStep);
activeFinishLandingPlatform.transform.position = new Vector2(spawnX, landingStep.yPosition);
activeFinishLandingPlatform.SetActive(true);
}
public void ClearFinishLandingPlatform() {
if(activeFinishLandingPlatform != null)
{
Destroy(activeFinishLandingPlatform);
activeFinishLandingPlatform = null;
}
}
public void StopCourseSpawningAfterFinishGate() {
courseSpawningStopped = true;
pendingTutorialGuides.Clear();
}
void Start() {
EnsurePatternSteps();
@@ -205,6 +249,11 @@ public class PlatformSpawner : MonoBehaviour {
return;
}
if(courseSpawningStopped)
{
return;
}
UpdatePendingTutorialGuides();
if(GameManager.instance != null && GameManager.instance.IsTutorialGuidePaused)
{
+1 -1
View File
@@ -5,7 +5,7 @@ using UnityEngine;
// PlayerController는 플레이어 캐릭터로서 Player 게임 오브젝트를 제어한다.
public class PlayerController : MonoBehaviour {
public AudioClip deathClip; // 사망시 재생할 오디오 클립
public float jumpForce = 700f; // 점프 힘
public float jumpForce = 600f; // 점프 힘
public int maxJumpCount = 2; // 최대 점프 횟수
public float invincibleDuration = 1.5f; // 피해를 입은 뒤 무적 시간
public float groundStickGraceTime = 0.12f; // 발판 이음새에서 접지가 아주 잠깐 끊겨도 유지하는 시간
+1 -1
View File
@@ -977,7 +977,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::PlayerController
deathClip: {fileID: 8300000, guid: 668a107660fa54fe98e03b698a09aa49, type: 3}
jumpForce: 700
jumpForce: 600
maxJumpCount: 2
invincibleDuration: 1.5
slidingColliderSize: {x: 1.2, y: 0.6}
+1 -1
View File
@@ -17,7 +17,7 @@
| `Assets/Resources/FinishGate_Tutorial_Incheon.png` | 튜토리얼 출국 게이트 |
| `Assets/Resources/FinishGate_Japan_Torii.png` | 일본 토리이 도착 지점 |
| `Assets/Resources/FinishGate_China_Paifang.png` | 중국 패방 도착 지점 |
| `Assets/Resources/FinishGate_America_Highway.png` | 미국 자유의 여신상 도착 지점 |
| `Assets/Resources/FinishGate_America_Liberty.png` | 미국 자유의 여신상 도착 지점 |
## Intro Assets
+3 -4
View File
@@ -96,7 +96,6 @@ Intro
## 다음 작업 우선순위
1. 변경된 도착 지점 이미지가 실제 게임에서 제대로 보이는지 확인
2. 면세점 구매 아이템을 장착 슬롯과 연결
3. 일본, 중국, 미국 맵의 실제 반복 패턴과 난이도 차별화
4. 미국 도착 지점 파일명 정리
1. 면세점 구매 아이템을 장착 슬롯과 연결
2. 일본, 중국, 미국 맵의 실제 반복 패턴과 난이도 차별화
3. 도착 지점 이미지의 실제 화면 크기를 플레이하면서 세부 조정
+1
View File
@@ -76,6 +76,7 @@ HUD:
5. 아이템 사용
설명은 화면을 가리지 않도록 잠깐 멈춘 뒤 보여주고, 이후 실제 구간을 통과하게 합니다.
아이템 설명창은 전체 아이템 목록을 한 번에 보여주지 않고, 현재 설명하는 아이템 이미지만 크게 보여줍니다.
## Result
+20 -7
View File
@@ -27,18 +27,31 @@
- 하단 미니맵을 런타임 HUD에서 제거
- 아이템 획득 시 짧은 링/반짝임/문구 이펙트 추가
- 라멘 무적/방어막 방어 성공 시 피격 깜빡임 대신 `SAFE`/`BLOCK` 피드백 표시
- 점프 마일리지 코인 경로를 5개 포물선으로 조정
- 점프 조작성을 쉽게 맞추기 위해 플레이어 점프 힘을 `700`에서 `600`으로 낮춤
- 속도가 붙을수록 점프 코인 경로의 가로 폭이 넓어지도록 보정
- 점프 코인 아크의 기본 가로/세로 폭을 더 크게 조정
- 출국 게이트 도착지는 만든 PNG 리소스를 우선 사용하도록 로딩 순서 수정
- 점프 코인 기본 아크 폭을 `1.3`, 최대 속도 폭을 `2.0`으로 확대
- 도착지점 스프라이트 로딩을 `Sprite`, `LoadAll<Sprite>`, `Texture2D`, 에디터 직접 경로 순서로 보강
- 점프 코인 기본 아크 폭을 다시 `1.5`, 최대 속도 폭을 `2.15`로 확대
- 점프 코인 기본 가로 폭만 `1.8`로 추가 확대
- 점프 코인 최대 속도 가로 폭을 `2.5`로 확대
- 도착 게이트가 나온 뒤에는 새 발판/장애물 코스가 더 스폰되지 않도록 수정
- 아이템 설명창은 전체 아이템 줄 대신 해당 아이템 하나만 큰 이미지로 표시
- 도착 게이트가 생성될 때 전용 착지 발판을 함께 생성하도록 수정
- 미국 도착 지점 리소스 파일명을 `FinishGate_America_Liberty.png`로 정리
- 라멘 무적 아이템 지속 시간이 현재 `2.2초`임을 문서에 기록
### 현재 문제
- 면세점 구매 아이템이 아직 장착 슬롯과 연결되지 않음
- 미국 도착 지점 파일명이 아직 `FinishGate_America_Highway.png`라 실제 내용과 이름이 다름
- 일본, 중국, 미국 맵의 실제 패턴과 난이도 차별화가 더 필요함
- 도착 지점 이미지의 게임 내 크기와 위치를 실제 플레이로 확인해야 함
- 도착 지점 이미지의 게임 내 세부 크기는 실제 플레이로 더 조정해야 함
### 다음 할 일
1. 도착 지점 이미지 크기와 위치 플레이 확인
2. 면세점 구매 아이템을 장착 슬롯과 연결
3. 일본, 중국, 미국 맵의 실제 패턴과 난이도 차별화
4. 미국 도착 지점 파일명 정리 검토
5. 컨셉 이펙트 이미지를 실제 개별 스프라이트로 분리할지 결정
1. 면세점 구매 아이템을 장착 슬롯과 연결
2. 일본, 중국, 미국 맵의 실제 패턴과 난이도 차별화
3. 도착 지점 이미지의 실제 화면 크기를 플레이하면서 세부 조정
4. 컨셉 이펙트 이미지를 실제 개별 스프라이트로 분리할지 결정
+5
View File
@@ -54,6 +54,11 @@
| 마일리지 카드 | 마일리지 획득량 증가 | 먹은 뒤 마일리지 수집 |
아이템을 먹으면 짧은 링, 반짝임, 문구 이펙트가 떠서 획득 여부를 확인할 수 있습니다.
라멘 무적 아이템의 현재 지속 시간은 `2.2초`입니다.
점프 구간의 마일리지 코인은 플레이어가 뛰는 높이에 맞춰 넓고 높은 포물선으로 배치합니다.
게임 속도가 빨라지면 코인 포물선의 가로 폭도 함께 넓혀 점프 타이밍과 맞춥니다.
현재 점프 코인 가로 폭은 기본 속도에서 `1.8배`, 최대 속도에서 `2.5배`까지 보정합니다.
## 코스 생성
+3 -3
View File
@@ -97,9 +97,9 @@
도착 지점:
- `Assets/Resources/FinishGate_America_Highway.png`
- 현재 파일명은 기존 하이웨이 이름이지만, 실제 이미지는 자유의 여신상/뉴욕 도착지입니다.
- 추후 파일명을 `FinishGate_America_Liberty.png`로 바꾸는 것이 더 명확합니다.
- `Assets/Resources/FinishGate_America_Liberty.png`
- 도착 게이트가 나올 때 전용 착지 발판을 함께 깔아 이미지가 공중에 뜬 것처럼 보이지 않게 합니다.
- 도착 게이트 뒤에는 추가 코스가 생성되지 않게 막아 클리어 지점이 끝처럼 보이게 합니다.
스킨:
+2
View File
@@ -100,6 +100,8 @@ Intro
- 미국: 자유의 여신상/뉴욕 도착지
플레이어가 도착 지점 트리거를 통과하면 클리어 처리합니다.
도착 게이트가 생성될 때는 `PlatformSpawner`가 빈 착지 발판을 같은 위치에 함께 깔아, 게이트가 배경 위에 떠 보이지 않게 합니다.
도착 게이트 뒤로는 새 발판, 장애물, 아이템 코스가 더 생성되지 않도록 스폰을 중지합니다.
## Minimap
+5 -5
View File
@@ -50,19 +50,19 @@ Intro
- 튜토리얼 인천공항 맵
- 패턴 기반 발판/장애물/아이템 배치
- 점프, 2단 점프, 슬라이드 튜토리얼
- 속도에 맞춰 가로 폭이 보정되는 점프 마일리지 코인 아크
- 목숨, 속도 감소, 무적, 방어막, 마일리지 카드 등 아이템 효과
- 아이템 획득/방어 성공 피드백 이펙트
- 맵 데이터 구조: `Tutorial / Incheon`, `Japan`, `China`, `America`
- 맵별 배경과 도착 지점 이미지 연결
- 맵별 배경과 도착 지점 이미지, 도착용 착지 발판, 게이트 뒤 스폰 중지 연결
- 저장/로드, 맵 해금, 결과 화면, 면세점 기본 흐름
## 다음 우선순위
현재 가장 가까운 작업은 다음입니다.
1. 도착 지점 이미지가 실제 게임에서 적절한 크기와 위치로 보이는지 확인
2. 면세점 구매 아이템을 장착 슬롯과 실제 플레이 준비 구조에 연결
3. 일본, 중국, 미국 맵의 실제 반복 패턴과 난이도 차별화
4. 미국 도착 지점 파일명 정리
1. 면세점 구매 아이템을 장착 슬롯과 실제 플레이 준비 구조에 연결
2. 일본, 중국, 미국 맵의 실제 반복 패턴과 난이도 차별화
3. 도착 지점 이미지의 실제 화면 크기를 플레이하면서 세부 조정
자세한 작업 기록과 다음 할 일은 [Docs/feedback.md](Docs/feedback.md)에 날짜별로 남깁니다.