convert game playing..

This commit is contained in:
2026-09-23 12:53:52 +09:00
parent 37c7942b71
commit 5d11288a6f
136 changed files with 15856 additions and 299 deletions
@@ -331,18 +331,19 @@ namespace BumpCombat.UI
24,
TextAnchor.MiddleLeft,
Vector2.zero,
new Vector2(220f, 110f));
new Vector2(240f, 220f));
RectTransform hintRect = hintText.rectTransform;
hintRect.anchorMin = new Vector2(0f, 1f);
hintRect.anchorMax = new Vector2(0f, 1f);
hintRect.pivot = new Vector2(0f, 1f);
hintRect.anchoredPosition = new Vector2(24f, -250f);
hintRect.anchoredPosition = new Vector2(24f, -230f);
hintText.supportRichText = true;
hintText.resizeTextForBestFit = false;
hintText.horizontalOverflow = HorizontalWrapMode.Wrap;
hintText.verticalOverflow = VerticalWrapMode.Overflow;
hintText.text = "<size=36><color=#FFD675><b>TAB</b></color></size>\n"
+ "<size=24>(미션 / 업적)</size>";
hintText.text = "<size=32><color=#FFD675><b>TAB</b></color></size> <size=20>(미션/업적)</size>\n\n"
+ "<size=18><color=#79E5FF><b>조작 안내</b></color></size>\n"
+ "<size=16><color=#FFFFFF>방향키 [▲▼◀▶]: 이동\n[A]: 스킬 / [D]: 가드\n[S]: 교체 / [M]: 지도</color></size>";
hintText.color = Color.white;
}
+137
View File
@@ -0,0 +1,137 @@
using BumpCombat.Audio;
using BumpCombat.Core;
using BumpCombat.Progression;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace BumpCombat.UI
{
public sealed class RunClearPanel : MonoBehaviour
{
private GameObject panel;
private Text titleText;
private Text statsText;
private Text restartText;
private bool isShown;
private void Start()
{
CreatePanel();
if (RunManager.Instance != null)
{
RunManager.Instance.OnRunCleared += HandleRunCleared;
}
}
private void OnDestroy()
{
if (RunManager.Instance != null)
{
RunManager.Instance.OnRunCleared -= HandleRunCleared;
}
}
private void Update()
{
if (!isShown || Keyboard.current == null)
{
return;
}
if (Keyboard.current.rKey.wasPressedThisFrame)
{
RunManager.Instance?.RestartRun();
}
else if (Keyboard.current.escapeKey.wasPressedThisFrame)
{
RunManager.Instance?.RestartToTitle();
}
}
private void HandleRunCleared()
{
isShown = true;
panel.SetActive(true);
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
float elapsed = RunManager.Instance != null ? RunManager.Instance.ElapsedTime : 0f;
int minutes = Mathf.FloorToInt(elapsed / 60f);
int seconds = Mathf.FloorToInt(elapsed % 60f);
ExperienceSystem exp = FindAnyObjectByType<ExperienceSystem>();
int level = exp != null ? exp.Level : 1;
statsText.text = RunManager.Instance != null && RunManager.Instance.IsDefensePrototypeRun ? $"CLEAR TIME: {minutes:D2}:{seconds:D2}\nCONSTRUCTION REWARD COLLECTED\nDEFENSE TEST CLEARED" : $"CLEAR TIME: {minutes:D2}:{seconds:D2}\nFINAL LEVEL: LV {level}\nSTAGE: 3 / 3 COMPLETED";
}
private void CreatePanel()
{
Canvas canvas = GetComponentInParent<Canvas>() ?? FindAnyObjectByType<Canvas>();
if (canvas == null)
{
return;
}
panel = new GameObject("RunClearPanel");
panel.transform.SetParent(canvas.transform, false);
RectTransform panelRect = panel.AddComponent<RectTransform>();
panelRect.anchorMin = Vector2.zero;
panelRect.anchorMax = Vector2.one;
panelRect.offsetMin = Vector2.zero;
panelRect.offsetMax = Vector2.zero;
// Background dark dim with gold tint
Image bg = panel.AddComponent<Image>();
bg.color = new Color(0.08f, 0.08f, 0.04f, 0.94f);
// Title
GameObject titleObj = new GameObject("ClearTitle");
titleObj.transform.SetParent(panel.transform, false);
titleText = titleObj.AddComponent<Text>();
titleText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
titleText.fontSize = 42;
titleText.fontStyle = FontStyle.Bold;
titleText.alignment = TextAnchor.MiddleCenter;
titleText.color = new Color(1f, 0.85f, 0.25f);
titleText.text = RunManager.Instance != null && RunManager.Instance.IsDefensePrototypeRun ? "★ DEFENSE TEST CLEARED ★" : "★ RUN CLEAR ★";
RectTransform titleRect = titleObj.GetComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0f, 0.6f);
titleRect.anchorMax = new Vector2(1f, 0.8f);
titleRect.offsetMin = Vector2.zero;
titleRect.offsetMax = Vector2.zero;
// Stats
GameObject statsObj = new GameObject("ClearStats");
statsObj.transform.SetParent(panel.transform, false);
statsText = statsObj.AddComponent<Text>();
statsText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
statsText.fontSize = 20;
statsText.alignment = TextAnchor.MiddleCenter;
statsText.color = new Color(0.9f, 0.9f, 0.95f);
statsText.lineSpacing = 1.4f;
RectTransform statsRect = statsObj.GetComponent<RectTransform>();
statsRect.anchorMin = new Vector2(0.1f, 0.35f);
statsRect.anchorMax = new Vector2(0.9f, 0.55f);
statsRect.offsetMin = Vector2.zero;
statsRect.offsetMax = Vector2.zero;
// Restart Hint
GameObject restartObj = new GameObject("ClearRestartHint");
restartObj.transform.SetParent(panel.transform, false);
restartText = restartObj.AddComponent<Text>();
restartText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
restartText.fontSize = 16;
restartText.alignment = TextAnchor.MiddleCenter;
restartText.color = new Color(0.7f, 0.7f, 0.75f);
restartText.text = "[R] 키로 재시작 | [ESC] 키로 타이틀로 이동";
RectTransform restartRect = restartObj.GetComponent<RectTransform>();
restartRect.anchorMin = new Vector2(0f, 0.15f);
restartRect.anchorMax = new Vector2(1f, 0.25f);
restartRect.offsetMin = Vector2.zero;
restartRect.offsetMax = Vector2.zero;
panel.SetActive(false);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5c63f44ba2673624d9e2fcdb546c8268
+214 -77
View File
@@ -53,13 +53,17 @@ namespace BumpCombat.UI
private Button[] debugSpawnButtons = System.Array.Empty<Button>();
private GameObject minimapPanel;
private RectTransform minimapMapRect;
private RectTransform minimapTerrainRoot;
private RectTransform minimapMarkerRoot;
private RectTransform minimapViewportRoot;
private Image minimapPlayerMarker;
private Image[] minimapViewportEdges = System.Array.Empty<Image>();
private readonly List<Image> minimapEnemyMarkers = new();
private readonly Dictionary<Vector2Int, Image> minimapTerrainCells = new();
private EnemyController[] minimapEnemies = System.Array.Empty<EnemyController>();
private float minimapEnemyRefreshElapsed;
private Stage.ZoneInstance minimapTerrainZone;
private int minimapTerrainVisibilityRevision = -1;
private ArenaBounds arenaBounds;
// The panel sprite has transparent source padding. At multiplier 1,
// 6px of canvas space keeps the map inside the visible frame.
@@ -68,22 +72,9 @@ namespace BumpCombat.UI
private Image[] artifactSlotIcons = System.Array.Empty<Image>();
private readonly Stack<Text> damagePopupPool = new();
private readonly HashSet<Text> activeDamagePopups = new();
private static readonly RunTimedEvent[] NextBossEvents =
{
RunTimedEvent.MidBoss,
RunTimedEvent.FinalBoss,
};
private static readonly string[] NextBossLabels =
{
"다음 중간보스",
"다음 보스",
};
private int activeDamagePopupCount;
private int damagePopupLifecycleVersion;
private float compactHudFadeElapsed;
private int lastNextEventSecond = -1;
private bool lastNextEventUsesDebugTimes;
private bool lastNextEventIsProductionRun;
private static readonly Color PlayerDamagePopupColor =
new(244f / 255f, 117f / 255f, 112f / 255f, 1f);
private static readonly Color GuardReadyColor = new Color32(255, 205, 92, 255);
@@ -741,61 +732,25 @@ namespace BumpCombat.UI
new Vector2(0f, -100f),
new Vector2(420f, 28f));
ConfigureCompactHudText(eventText, 14, 18);
eventText.gameObject.SetActive(true);
eventText.gameObject.SetActive(false);
UpdateNextEventHud();
}
private void UpdateNextEventHud()
{
if (eventText == null || runManager == null || showingTimedEventMessage)
if (eventText == null)
{
return;
}
int elapsedSecond = Mathf.FloorToInt(runManager.ElapsedTime);
if (elapsedSecond == lastNextEventSecond
&& runManager.UseDebugEventTimes == lastNextEventUsesDebugTimes
&& runManager.IsProductionRun == lastNextEventIsProductionRun)
if (!showingTimedEventMessage)
{
return;
}
lastNextEventSecond = elapsedSecond;
lastNextEventUsesDebugTimes = runManager.UseDebugEventTimes;
lastNextEventIsProductionRun = runManager.IsProductionRun;
float nextTime = float.PositiveInfinity;
string nextLabel = "다음 이벤트 없음";
for (int i = 0; i < runManager.CurrentEliteEventCount; i++)
{
float eliteTime = runManager.GetEliteEventTimeForCurrentRun(i);
if (eliteTime > runManager.ElapsedTime)
if (eventText.gameObject.activeSelf)
{
nextTime = eliteTime;
nextLabel = "다음 엘리트";
break;
eventText.gameObject.SetActive(false);
}
}
for (int i = 0; i < NextBossEvents.Length; i++)
{
float eventTime = runManager.GetEventTimeForCurrentRun(
NextBossEvents[i]);
if (eventTime > runManager.ElapsedTime && eventTime < nextTime)
{
nextTime = eventTime;
nextLabel = NextBossLabels[i];
}
}
if (float.IsPositiveInfinity(nextTime))
{
eventText.text = nextLabel;
return;
}
int seconds = Mathf.FloorToInt(nextTime);
eventText.text =
$"{nextLabel} {seconds / 60:00}:{seconds % 60:00}";
}
private void LayoutDebugText()
@@ -844,6 +799,20 @@ namespace BumpCombat.UI
{
popupText = $"LAUNCH {popupText}";
}
EnemyController popupEnemy = result.Target != null
? result.Target.GetComponentInParent<EnemyController>()
: null;
Stage.ZoneInstance popupZone = Stage.StageManager.Instance != null
? Stage.StageManager.Instance.CurrentZone
: null;
if (popupEnemy != null
&& popupZone != null
&& !popupZone.IsWorldPositionVisible(popupEnemy.GroundAnchorPosition))
{
return;
}
Color popupColor;
if (!TryGetArtifactPopupColor(result, out popupColor))
{
@@ -1271,7 +1240,7 @@ namespace BumpCombat.UI
RunTimedEvent.MidBoss,
RunTimedEvent.FinalBoss,
};
string[] labels = { "엘리트 출현", "중간보스 출현", "보스 출현" };
string[] labels = { "엘리트 출현", "중간보스 출현", "보스방 이동" };
debugSpawnButtons = new Button[events.Length];
for (int i = 0; i < events.Length; i++)
{
@@ -1296,16 +1265,30 @@ namespace BumpCombat.UI
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = image;
PresentationUiStyle.ApplyButton(button);
RunTimedEvent requestedEvent = events[i];
button.onClick.AddListener(() =>
if (i == 2)
{
if (spawnDirector != null
&& spawnDirector.TryDebugSpawnEventEnemy(requestedEvent))
button.onClick.AddListener(() =>
{
HandleTimedEvent(requestedEvent);
}
UpdateDebugSpawnHud();
});
if (Stage.StageManager.Instance != null)
{
Stage.StageManager.Instance.CheatTeleportToBossRoom();
}
UpdateDebugSpawnHud();
});
}
else
{
RunTimedEvent requestedEvent = events[i];
button.onClick.AddListener(() =>
{
if (spawnDirector != null
&& spawnDirector.TryDebugSpawnEventEnemy(requestedEvent))
{
HandleTimedEvent(requestedEvent);
}
UpdateDebugSpawnHud();
});
}
CreateRuntimeText(
buttonObject.transform,
"Label",
@@ -1327,7 +1310,7 @@ namespace BumpCombat.UI
bool visible = runManager != null && runManager.UseDebugArtifactSelection;
debugSpawnPanel.SetActive(visible);
if (!visible || spawnDirector == null)
if (!visible)
{
return;
}
@@ -1338,10 +1321,19 @@ namespace BumpCombat.UI
RunTimedEvent.MidBoss,
RunTimedEvent.FinalBoss,
};
bool inputEnabled = RunManager.GameplayInputEnabled;
for (int i = 0; i < debugSpawnButtons.Length; i++)
{
debugSpawnButtons[i].interactable =
spawnDirector.CanDebugSpawnEventEnemy(events[i]);
if (i == 2)
{
debugSpawnButtons[i].interactable =
inputEnabled && Stage.StageManager.Instance != null;
}
else
{
debugSpawnButtons[i].interactable =
spawnDirector != null && spawnDirector.CanDebugSpawnEventEnemy(events[i]);
}
}
}
@@ -1370,7 +1362,7 @@ namespace BumpCombat.UI
new Vector2(panelWidth, panelHeight));
Image minimapFrame = minimapPanel.GetComponent<Image>();
PresentationUiStyle.ApplyPanel(minimapFrame);
minimapFrame.pixelsPerUnitMultiplier = 1f;
minimapFrame.pixelsPerUnitMultiplier = PresentationUiStyle.ThreePixelBorderMultiplier;
minimapFrame.fillCenter = false;
GameObject map = CreateRuntimePanel(
@@ -1384,6 +1376,7 @@ namespace BumpCombat.UI
map.GetComponent<Image>().color = new Color(0.035f, 0.06f, 0.08f, 0.28f);
map.AddComponent<RectMask2D>();
minimapTerrainRoot = CreateMinimapLayer(map.transform, "Minimap Terrain");
minimapMarkerRoot = CreateMinimapLayer(map.transform, "Markers");
minimapViewportRoot = CreateMinimapLayer(map.transform, "Minimap Camera Viewport");
minimapMarkerRoot.SetAsLastSibling();
@@ -1453,6 +1446,8 @@ namespace BumpCombat.UI
return;
}
UpdateMinimapTerrain();
SetMinimapMarkerPosition(
minimapPlayerMarker,
playerHealth != null ? playerHealth.transform.position : Vector3.zero,
@@ -1469,6 +1464,9 @@ namespace BumpCombat.UI
}
int markerIndex = 0;
Stage.ZoneInstance visibilityZone = Stage.StageManager.Instance != null
? Stage.StageManager.Instance.CurrentZone
: null;
for (int i = 0; i < minimapEnemies.Length; i++)
{
EnemyController enemy = minimapEnemies[i];
@@ -1476,6 +1474,16 @@ namespace BumpCombat.UI
{
continue;
}
if (worldCamera == null
|| !arenaBounds.IsInsideCameraVisible(worldCamera, enemy.GroundAnchorPosition))
{
continue;
}
if (visibilityZone != null
&& !visibilityZone.IsWorldPositionVisible(enemy.GroundAnchorPosition))
{
continue;
}
Image marker = GetMinimapEnemyMarker(markerIndex++);
marker.gameObject.SetActive(true);
@@ -1499,6 +1507,132 @@ namespace BumpCombat.UI
UpdateMinimapViewport(extents);
}
private void UpdateMinimapTerrain()
{
Stage.ZoneInstance currentZone = Stage.StageManager.Instance != null
? Stage.StageManager.Instance.CurrentZone
: null;
if (currentZone == minimapTerrainZone)
{
if (currentZone != null
&& currentZone.VisibilityRevision != minimapTerrainVisibilityRevision)
{
UpdateMinimapTerrainVisibility(currentZone);
}
return;
}
minimapTerrainZone = currentZone;
minimapTerrainVisibilityRevision = -1;
minimapTerrainCells.Clear();
if (minimapTerrainRoot == null)
{
return;
}
for (int i = minimapTerrainRoot.childCount - 1; i >= 0; i--)
{
Destroy(minimapTerrainRoot.GetChild(i).gameObject);
}
Stage.RoomPreset preset = currentZone != null ? currentZone.Preset : null;
if (preset == null || !currentZone.HasRuntimeGrid ||
preset.GridWidth <= 0 || preset.GridHeight <= 0)
{
return;
}
Vector2 mapSize = minimapMapRect.rect.size;
float cellWidth = mapSize.x / preset.GridWidth;
float cellHeight = mapSize.y / preset.GridHeight;
float left = -mapSize.x * 0.5f;
float top = mapSize.y * 0.5f;
for (int y = 0; y < preset.GridHeight; y++)
{
for (int x = 0; x < preset.GridWidth; x++)
{
Stage.PresetCellType cellType = currentZone.GetRuntimeCell(x, y);
if (cellType == Stage.PresetCellType.Empty)
{
continue;
}
Image cell = CreateRuntimeImage(
minimapTerrainRoot,
$"Terrain {x},{y}",
Vector2.zero,
new Vector2(cellWidth + 0.2f, cellHeight + 0.2f));
cell.raycastTarget = false;
minimapTerrainCells[new Vector2Int(x, y)] = cell;
SetRectTransform(
cell.rectTransform,
new Vector2(0.5f, 0.5f),
new Vector2(0.5f, 0.5f),
new Vector2(
left + (x + 0.5f) * cellWidth,
top - (y + 0.5f) * cellHeight),
cell.rectTransform.sizeDelta);
}
}
UpdateMinimapTerrainVisibility(currentZone);
}
private void UpdateMinimapTerrainVisibility(Stage.ZoneInstance zone)
{
if (zone == null)
{
return;
}
foreach (KeyValuePair<Vector2Int, Image> entry in minimapTerrainCells)
{
Vector2Int cell = entry.Key;
if (entry.Value == null)
{
continue;
}
entry.Value.color = GetMinimapTerrainColor(
zone.GetRuntimeCell(cell.x, cell.y),
zone.IsCellVisible(cell.x, cell.y),
zone.IsCellExplored(cell.x, cell.y));
}
minimapTerrainVisibilityRevision = zone.VisibilityRevision;
}
private static Color GetMinimapTerrainColor(
Stage.PresetCellType cellType,
bool visible,
bool explored)
{
Color baseColor = cellType switch
{
Stage.PresetCellType.Obstacle => new Color32(12, 18, 22, 240),
Stage.PresetCellType.Socket => new Color32(54, 103, 112, 230),
Stage.PresetCellType.RewardAnchor => new Color32(73, 88, 70, 205),
_ => new Color32(41, 57, 62, 190),
};
if (visible)
{
return baseColor;
}
if (explored)
{
return new Color(
baseColor.r * 0.35f,
baseColor.g * 0.35f,
baseColor.b * 0.35f,
1f);
}
return new Color32(2, 4, 5, 255);
}
private Image GetMinimapEnemyMarker(int index)
{
while (minimapEnemyMarkers.Count <= index)
@@ -1544,9 +1678,11 @@ namespace BumpCombat.UI
return;
}
Vector2 center = arenaBounds != null ? arenaBounds.ZoneCenter : Vector2.zero;
Vector2 localPos = worldPosition - center;
Vector2 normalized = new(
Mathf.Clamp01((worldPosition.x + extents.x) / (extents.x * 2f)),
Mathf.Clamp01((worldPosition.y + extents.y) / (extents.y * 2f)));
Mathf.Clamp01((localPos.x + extents.x) / (extents.x * 2f)),
Mathf.Clamp01((localPos.y + extents.y) / (extents.y * 2f)));
Vector2 halfMarker = marker.rectTransform.rect.size * 0.5f;
float halfMapWidth = mapSize.x * 0.5f;
float halfMapHeight = mapSize.y * 0.5f;
@@ -1575,8 +1711,9 @@ namespace BumpCombat.UI
Vector2 mapSize = minimapMapRect.rect.size;
Bounds visible = arenaBounds.GetCameraVisibleBounds(worldCamera);
Vector2 min = WorldToMinimapPosition(visible.min, extents, mapSize);
Vector2 max = WorldToMinimapPosition(visible.max, extents, mapSize);
Vector2 zoneCenter = arenaBounds != null ? arenaBounds.ZoneCenter : Vector2.zero;
Vector2 min = WorldToMinimapPosition(visible.min, extents, mapSize, zoneCenter);
Vector2 max = WorldToMinimapPosition(visible.max, extents, mapSize, zoneCenter);
float width = Mathf.Max(2f, max.x - min.x);
float height = Mathf.Max(2f, max.y - min.y);
Vector2 center = (min + max) * 0.5f;
@@ -1617,11 +1754,13 @@ namespace BumpCombat.UI
private static Vector2 WorldToMinimapPosition(
Vector2 worldPosition,
Vector2 extents,
Vector2 mapSize)
Vector2 mapSize,
Vector2 center = default)
{
Vector2 localPos = worldPosition - center;
Vector2 normalized = new(
Mathf.Clamp01((worldPosition.x + extents.x) / (extents.x * 2f)),
Mathf.Clamp01((worldPosition.y + extents.y) / (extents.y * 2f)));
Mathf.Clamp01((localPos.x + extents.x) / (extents.x * 2f)),
Mathf.Clamp01((localPos.y + extents.y) / (extents.y * 2f)));
return new Vector2(
(normalized.x - 0.5f) * mapSize.x,
(normalized.y - 0.5f) * mapSize.y);
@@ -1705,12 +1844,10 @@ namespace BumpCombat.UI
private IEnumerator ShowEventMessage(string message)
{
showingTimedEventMessage = true;
lastNextEventSecond = -1;
eventText.text = message;
eventText.gameObject.SetActive(true);
yield return new WaitForSecondsRealtime(3f);
showingTimedEventMessage = false;
lastNextEventSecond = -1;
UpdateNextEventHud();
}
@@ -1760,7 +1897,7 @@ namespace BumpCombat.UI
"Presentation/Fonts/NanumGothic-Regular";
private const string NeoFontResource = "Presentation/Fonts/neodgm";
private const string GalmuriFontResource = "Presentation/Fonts/Galmuri9";
private const float ThreePixelBorderMultiplier = 1f / 3f;
internal const float ThreePixelBorderMultiplier = 1f / 3f;
private static readonly Color PanelFallback =
new(24f / 255f, 27f / 255f, 39f / 255f, 0.94f);
@@ -456,6 +456,11 @@ namespace BumpCombat.UI
BeginRun(RunMode.Production);
}
private void BeginDefensePrototypeRun()
{
BeginRun(RunMode.DefensePrototype);
}
private void OpenTitleModes()
{
SetScreen(Screen.TitleModes);
@@ -469,6 +474,10 @@ namespace BumpCombat.UI
ShowButton(index++, "개발용 시작", BeginDevelopmentRun);
}
ShowButton(index++, "기본 모드", BeginProductionRun);
if (IsDevelopmentStartVisible(Application.isEditor, Debug.isDebugBuild))
{
ShowButton(index++, "방어전 테스트", BeginDefensePrototypeRun);
}
ShowDisabledButton(index++, "챌린지 모드(준비 중)");
ShowDisabledButton(index++, "하드코어 모드(준비 중)");
ShowButton(index, "뒤로", ReturnToTitleMenu);
@@ -650,7 +659,7 @@ namespace BumpCombat.UI
inputHelpText.alignment = TextAnchor.MiddleLeft;
inputHelpText.color = new Color32(200, 208, 218, 255);
CreateSettingsFocusRows(panel.transform);
for (int i = 0; i < 5; i++)
for (int i = 0; i < 6; i++)
{
Button button = CreateButton(panel.transform, $"Button {i + 1}", font, new Vector2(0f, 95f - i * 88f));
button.gameObject.SetActive(false);
+340
View File
@@ -0,0 +1,340 @@
using System.Collections.Generic;
using BumpCombat.Audio;
using BumpCombat.Core;
using BumpCombat.Stage;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UI;
namespace BumpCombat.UI
{
public sealed class WorldMapPanel : MonoBehaviour
{
private GameObject panel;
private Text titleText;
private Text seedText;
private RectTransform mapContainer;
private readonly List<GameObject> spawnedElements = new();
private bool isOpen;
private RunManager runManager;
public bool IsOpen => isOpen;
private void Start()
{
runManager = RunManager.Instance;
CreatePanel();
}
private void Update()
{
if (Keyboard.current == null || runManager == null)
{
return;
}
if (Keyboard.current.mKey.wasPressedThisFrame)
{
if (isOpen)
{
CloseMap();
}
else
{
OpenMap();
}
}
else if (isOpen && Keyboard.current.escapeKey.wasPressedThisFrame)
{
CloseMap();
}
}
public void OpenMap()
{
if (isOpen || runManager == null || runManager.IsTitleScreen || runManager.IsGameOver || runManager.IsRunCleared)
{
return;
}
if (!runManager.TryOpenSelection(this))
{
return;
}
isOpen = true;
panel.SetActive(true);
BumpCombatAudioService.Instance?.PlayUi("ui_confirm");
RefreshMap();
}
public void CloseMap()
{
if (!isOpen)
{
return;
}
isOpen = false;
panel.SetActive(false);
runManager?.SuppressPauseOpeningForCurrentFrame();
runManager?.CloseSelection(this);
BumpCombatAudioService.Instance?.PlayUi("ui_cancel");
}
private void CreatePanel()
{
Canvas canvas = GetComponentInParent<Canvas>() ?? FindAnyObjectByType<Canvas>();
if (canvas == null)
{
return;
}
panel = new GameObject("WorldMapPanel");
panel.transform.SetParent(canvas.transform, false);
RectTransform panelRect = panel.AddComponent<RectTransform>();
panelRect.anchorMin = Vector2.zero;
panelRect.anchorMax = Vector2.one;
panelRect.offsetMin = Vector2.zero;
panelRect.offsetMax = Vector2.zero;
// Background dark dim
Image bg = panel.AddComponent<Image>();
bg.color = new Color(0.05f, 0.06f, 0.1f, 0.92f);
// Top Header: Title & Seed
GameObject titleObj = new GameObject("MapTitle");
titleObj.transform.SetParent(panel.transform, false);
titleText = titleObj.AddComponent<Text>();
titleText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
titleText.fontSize = 28;
titleText.fontStyle = FontStyle.Bold;
titleText.alignment = TextAnchor.MiddleCenter;
titleText.color = new Color(0.9f, 0.85f, 0.6f);
RectTransform titleRect = titleObj.GetComponent<RectTransform>();
titleRect.anchorMin = new Vector2(0f, 1f);
titleRect.anchorMax = new Vector2(1f, 1f);
titleRect.pivot = new Vector2(0.5f, 1f);
titleRect.anchoredPosition = new Vector2(0f, -25f);
titleRect.sizeDelta = new Vector2(0f, 40f);
GameObject seedObj = new GameObject("MapSeed");
seedObj.transform.SetParent(panel.transform, false);
seedText = seedObj.AddComponent<Text>();
seedText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
seedText.fontSize = 16;
seedText.alignment = TextAnchor.MiddleCenter;
seedText.color = new Color(0.7f, 0.7f, 0.8f);
RectTransform seedRect = seedObj.GetComponent<RectTransform>();
seedRect.anchorMin = new Vector2(0f, 1f);
seedRect.anchorMax = new Vector2(1f, 1f);
seedRect.pivot = new Vector2(0.5f, 1f);
seedRect.anchoredPosition = new Vector2(0f, -65f);
seedRect.sizeDelta = new Vector2(0f, 25f);
// Container for grid nodes
GameObject containerObj = new GameObject("MapContainer");
containerObj.transform.SetParent(panel.transform, false);
mapContainer = containerObj.AddComponent<RectTransform>();
mapContainer.anchorMin = new Vector2(0.1f, 0.1f);
mapContainer.anchorMax = new Vector2(0.9f, 0.85f);
mapContainer.offsetMin = Vector2.zero;
mapContainer.offsetMax = Vector2.zero;
// Bottom guide
GameObject guideObj = new GameObject("MapGuide");
guideObj.transform.SetParent(panel.transform, false);
Text guideText = guideObj.AddComponent<Text>();
guideText.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
guideText.fontSize = 16;
guideText.fontStyle = FontStyle.Bold;
guideText.alignment = TextAnchor.MiddleCenter;
guideText.color = new Color(1f, 0.85f, 0.35f, 1f); // Prominent yellow
guideText.text = "※ 지도는 확인용입니다. 창을 닫고 캐릭터를 움직여 방의 [출구 ▶]로 들어가면 다음 구역으로 진입합니다.\n[M] 또는 [ESC] 키를 눌러 지도를 닫으세요.";
RectTransform guideRect = guideObj.GetComponent<RectTransform>();
guideRect.anchorMin = new Vector2(0f, 0f);
guideRect.anchorMax = new Vector2(1f, 0f);
guideRect.pivot = new Vector2(0.5f, 0f);
guideRect.anchoredPosition = new Vector2(0f, 15f);
guideRect.sizeDelta = new Vector2(0f, 45f);
panel.SetActive(false);
}
private void RefreshMap()
{
// Clear previous elements
for (int i = 0; i < spawnedElements.Count; i++)
{
if (spawnedElements[i] != null)
{
Destroy(spawnedElements[i]);
}
}
spawnedElements.Clear();
StageManager stageManager = StageManager.Instance;
if (stageManager == null || stageManager.CurrentPlan == null)
{
titleText.text = "STAGE WORLD MAP";
seedText.text = "NO ACTIVE STAGE";
return;
}
StagePlan plan = stageManager.CurrentPlan;
titleText.text = $"STAGE {plan.StageIndex} — {GetStageSubtitle(plan.StageIndex)}";
seedText.text = $"SEED: {stageManager.CurrentSeed}";
RectInt bounds = plan.GetGridBounds();
float cellWidth = 140f;
float cellHeight = 90f;
Vector2 origin = new Vector2(
-(bounds.x + bounds.width * 0.5f - 0.5f) * cellWidth,
-(bounds.y + bounds.height * 0.5f - 0.5f) * cellHeight);
// Render Connection Lines first
foreach (StageNode node in plan.Nodes)
{
Vector2 nodePos = origin + new Vector2(node.GridPosition.x * cellWidth, node.GridPosition.y * cellHeight);
for (int d = 0; d < 4; d++)
{
StageDirection dir = (StageDirection)d;
int targetId = node.GetConnection(dir);
if (targetId > node.Id) // Draw each line once
{
StageNode targetNode = plan.GetNode(targetId);
if (targetNode != null)
{
Vector2 targetPos = origin + new Vector2(targetNode.GridPosition.x * cellWidth, targetNode.GridPosition.y * cellHeight);
CreateConnectionLine(nodePos, targetPos);
}
}
}
}
// Render Nodes
foreach (StageNode node in plan.Nodes)
{
Vector2 nodePos = origin + new Vector2(node.GridPosition.x * cellWidth, node.GridPosition.y * cellHeight);
CreateNodeCard(node, nodePos);
}
}
private static string GetStageSubtitle(int stageIndex)
{
return stageIndex switch
{
1 => "ELITE STAGE",
2 => "MID BOSS STAGE",
3 => "FINAL BOSS STAGE",
_ => "EXPLORATION",
};
}
private void CreateConnectionLine(Vector2 from, Vector2 to)
{
GameObject lineObj = new GameObject("MapLine");
lineObj.transform.SetParent(mapContainer, false);
Image lineImg = lineObj.AddComponent<Image>();
lineImg.color = new Color(0.3f, 0.4f, 0.55f, 0.6f);
RectTransform rt = lineObj.GetComponent<RectTransform>();
Vector2 diff = to - from;
rt.sizeDelta = new Vector2(diff.magnitude, 4f);
rt.pivot = new Vector2(0f, 0.5f);
rt.anchoredPosition = from;
float angle = Mathf.Atan2(diff.y, diff.x) * Mathf.Rad2Deg;
rt.localRotation = Quaternion.Euler(0f, 0f, angle);
spawnedElements.Add(lineObj);
}
private void CreateNodeCard(StageNode node, Vector2 anchoredPos)
{
GameObject nodeObj = new GameObject($"NodeCard_{node.Id}");
nodeObj.transform.SetParent(mapContainer, false);
RectTransform rt = nodeObj.AddComponent<RectTransform>();
rt.sizeDelta = new Vector2(110f, 65f);
rt.anchoredPosition = anchoredPos;
Image bg = nodeObj.AddComponent<Image>();
bg.color = GetNodeColor(node);
// Node Text: Name / Type
GameObject textObj = new GameObject("Label");
textObj.transform.SetParent(nodeObj.transform, false);
Text label = textObj.AddComponent<Text>();
label.font = Resources.GetBuiltinResource<Font>("LegacyRuntime.ttf");
label.fontSize = 12;
label.fontStyle = FontStyle.Bold;
label.alignment = TextAnchor.MiddleCenter;
label.color = Color.white;
label.text = GetNodeDisplayText(node);
RectTransform textRt = textObj.GetComponent<RectTransform>();
textRt.anchorMin = Vector2.zero;
textRt.anchorMax = Vector2.one;
textRt.offsetMin = new Vector2(4f, 4f);
textRt.offsetMax = new Vector2(-4f, -4f);
// Current Player Indicator Border
if (node.IsCurrent)
{
GameObject borderObj = new GameObject("CurrentBorder");
borderObj.transform.SetParent(nodeObj.transform, false);
Image borderImg = borderObj.AddComponent<Image>();
borderImg.color = new Color(1f, 0.85f, 0.2f, 0.9f);
RectTransform borderRt = borderObj.GetComponent<RectTransform>();
borderRt.anchorMin = Vector2.zero;
borderRt.anchorMax = Vector2.one;
borderRt.offsetMin = new Vector2(-4f, -4f);
borderRt.offsetMax = new Vector2(4f, 4f);
borderObj.transform.SetAsFirstSibling();
}
spawnedElements.Add(nodeObj);
}
private static Color GetNodeColor(StageNode node)
{
if (node.IsCurrent)
{
return new Color(0.2f, 0.5f, 0.8f, 0.95f);
}
return node.NodeType switch
{
StageNodeType.Entrance => new Color(0.25f, 0.45f, 0.35f, 0.9f),
StageNodeType.Combat => new Color(0.35f, 0.25f, 0.25f, 0.9f),
StageNodeType.Fork => new Color(0.3f, 0.3f, 0.45f, 0.9f),
StageNodeType.Reward => new Color(0.5f, 0.4f, 0.15f, 0.9f),
StageNodeType.Recovery => new Color(0.2f, 0.45f, 0.25f, 0.9f),
StageNodeType.Elite => new Color(0.6f, 0.25f, 0.15f, 0.95f),
StageNodeType.MidBoss => new Color(0.65f, 0.2f, 0.2f, 0.95f),
StageNodeType.FinalBoss => new Color(0.6f, 0.15f, 0.5f, 0.95f),
StageNodeType.StageExit => new Color(0.2f, 0.55f, 0.45f, 0.9f),
_ => new Color(0.3f, 0.3f, 0.35f, 0.9f),
};
}
private static string GetNodeDisplayText(StageNode node)
{
string currentTag = node.IsCurrent ? "[YOU]\n" : "";
string typeName = node.NodeType switch
{
StageNodeType.Entrance => "ENTRANCE",
StageNodeType.Combat => "COMBAT",
StageNodeType.Fork => "FORK",
StageNodeType.Reward => $"REWARD\n<{node.RewardType}>",
StageNodeType.Recovery => "RECOVERY\n<HEAL 50%>",
StageNodeType.Elite => "★ ELITE ★",
StageNodeType.MidBoss => "★ MID BOSS ★",
StageNodeType.FinalBoss => "☠ NECROMANCER ☠",
StageNodeType.StageExit => "STAGE EXIT",
_ => node.NodeType.ToString().ToUpper(),
};
return currentTag + typeName;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3ac4129f6b1ac4a4789bc13d9ad8a435